<aside>

Overview

:ambient-logo-original: Ambient Documentation

What is Ambient?

Getting Started

Testnet

App

API

On-Chain

Tool Oracle

Claude Code

Miners

Encyclopedia

Solana Quickstart

X402 Client Documentation

Developers

OpenAI SDK

Anthropic SDK

API Docs

API Playground

Links

🌐 Homepage

✉️ [email protected]

:discord-symbol-blurple: Discord

:github-mark-white-bg: github.com/ambient-xyz

:x-logo-black: @ambient_xyz

</aside>

Overview

JumpGate supports pay-per-request AI inference through the x402 payment protocol. Clients send a chat request, receive a quote for that exact request, then use an x402 client to pay with USDC and stream the response.

Recommended endpoint: POST <https://jumpgate.ambient.xyz/paid/chat/v2>

Quote endpoint: POST <https://jumpgate.ambient.xyz/paid/chat/v2/quote>

New clients should use /paid/chat/v2. /paid/chat may remain available for older x402 v1 clients, but it is not the recommended integration path.

Legacy vault-backed relayer endpoints are no longer the supported integration path.


Supported Payment Networks

JumpGate accepts USDC on:

USDC token addresses:

Use Solana when you want lower network fees. Use Base when your application already has EVM wallet infrastructure.


Prerequisites

Before making a paid request, your client needs:


How x402 Works

The x402 protocol uses the HTTP 402 Payment Required response to negotiate payment for a normal HTTP request.

For JumpGate:

  1. Your client sends the paid chat request to JumpGate.
  2. JumpGate returns x402 payment requirements for Solana and Base.
  3. Your x402 client selects a supported network and signs the USDC payment.
  4. The client retries the request with payment proof.
  5. JumpGate verifies the payment and streams the inference response as SSE.

The official x402 client libraries handle the payment negotiation and retry. Your application still controls the request body, pricing quote, wallet selection, and local spend cap.


Request Flow

1. Build the inference request body.
2. POST the same body to /paid/chat/v2/quote, plus optional max_tool_calls.
3. Read quote.headers, amount fields, token counts, model_tier, and max spend fields.
4. POST the original inference request to /paid/chat/v2 with quote.headers copied unchanged.
5. Let the x402 client handle 402 -> sign/pay -> retry.
6. Read the final response as Server-Sent Events.

The quote endpoint uses the same chat tokenization and pricing validation as /paid/chat/v2, so clients should not estimate token counts locally.


Quote Request

Send the same inference body you will send to /paid/chat/v2. Add max_tool_calls when your client wants to reserve budget for tool calls.

{
  "model": "ambient/large",
  "messages": [
    {
      "role": "user",
      "content": "Explain verifiable inference in one paragraph."
    }
  ],
  "stream": true,
  "is_paid": true,
  "max_completion_tokens": 1000,
  "max_tool_calls": 0
}

Quote response:

{
  "headers": {
    "x402-input-tokens": "17",
    "x402-max-completion-tokens": "1000",
    "x402-model-tier": "standard",
    "x402-max-tool-calls": "0"
  },
  "input_tokens": 17,
  "output_tokens": 1000,
  "model_tier": "standard",
  "max_tool_calls": 0,
  "amount_micro_usdc": 4424,
  "amount_usdc": 0.004424,
  "max_amount_micro_usdc": 4474,
  "max_amount_usdc": 0.004474
}

Copy headers unchanged into the paid chat request. Use max_amount_micro_usdc as the local spend cap for the x402 payment requirement.


Pricing Headers

The quote response returns the pricing headers required by /paid/chat/v2:

Do not calculate these headers from byte length or character length. Request a quote and forward the returned values.

For human-readable pricing, see Ambient Billing.


TypeScript Client

Install the official x402 packages and wallet libraries:

npm install @x402/core @x402/fetch @x402/svm @x402/evm viem @solana/kit @solana/web3.js

Shared request and quote helpers:

const JUMPGATE_URL = '<https://jumpgate.ambient.xyz>';
const PAID_CHAT_URL = `${JUMPGATE_URL}/paid/chat/v2`;
const QUOTE_URL = `${PAID_CHAT_URL}/quote`;

const request = {
  model: 'ambient/large',
  messages: [{ role: 'user', content: 'Hello from x402.' }],
  stream: true,
  is_paid: true,
  max_completion_tokens: 1000
};

async function quoteChat(request: unknown) {
  const response = await fetch(QUOTE_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ ...request, max_tool_calls: 0 })
  });

  if (!response.ok) throw new Error(await response.text());
  return await response.json();
}

Payment and streaming helper:

import { x402Client } from '@x402/core/client';
import { wrapFetchWithPayment } from '@x402/fetch';

function paymentAmountMicroUsdc(requirement: { amount?: string; maxAmountRequired?: string }) {
  return BigInt(requirement.amount ?? requirement.maxAmountRequired ?? 0);
}

async function streamPaidChat({ quote, request, registerScheme }) {
  const client = new x402Client();
  const maxAmount = BigInt(quote.max_amount_micro_usdc ?? quote.amount_micro_usdc);

  client.registerPolicy((_version, requirements) =>
    requirements.filter((requirement) => paymentAmountMicroUsdc(requirement) <= maxAmount)
  );

  registerScheme(client);

  const fetchWithPayment = wrapFetchWithPayment(fetch, client);
  const response = await fetchWithPayment(PAID_CHAT_URL, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Accept: 'text/event-stream',
      ...quote.headers
    },
    body: JSON.stringify(request)
  });

  if (!response.ok) throw new Error(await response.text());

  for await (const payload of readSse(response)) {
    console.log(payload);
  }
}

SSE parser:

async function* readSse(response: Response) {
  const reader = response.body?.getReader();
  if (!reader) throw new Error('Response body is not readable.');

  const decoder = new TextDecoder();
  let buffer = '';

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    buffer += decoder.decode(value, { stream: true });
    const frames = buffer.split(/\\r?\\n\\r?\\n/);
    buffer = frames.pop() || '';

    for (const frame of frames) {
      const data = frame
        .split(/\\r?\\n/)
        .filter((line) => line.startsWith('data:'))
        .map((line) => line.slice(5).trimStart())
        .join('\\n');

      if (data && data !== '[DONE]') yield JSON.parse(data);
    }
  }
}

Solana signer

Use a connected Solana wallet that can sign transactions.

import { getBase64EncodedWireTransaction, address as solanaAddress } from '@solana/kit';
import { VersionedTransaction } from '@solana/web3.js';
import { toClientSvmSigner } from '@x402/svm';
import { registerExactSvmScheme } from '@x402/svm/exact/client';

function createSolanaSigner(provider, publicKeyString: string) {
  return toClientSvmSigner({
    address: solanaAddress(publicKeyString),
    signTransactions: async (transactions) =>
      await Promise.all(
        transactions.map(async (transaction) => {
          const encoded = getBase64EncodedWireTransaction(transaction);
          const web3Transaction = VersionedTransaction.deserialize(base64ToBytes(encoded));
          const signed = await provider.signTransaction(web3Transaction);
          const signerIndex = signed.message.staticAccountKeys.findIndex(
            (key) => key.toBase58() === publicKeyString
          );

          return Object.freeze({ [publicKeyString]: signed.signatures[signerIndex] });
        })
      )
  });
}

const solanaSigner = createSolanaSigner(window.solana, window.solana.publicKey.toBase58());
const quote = await quoteChat(request);
await streamPaidChat({
  quote,
  request,
  registerScheme: (client) => registerExactSvmScheme(client, { signer: solanaSigner })
});

Base signer

Use a Base-compatible EVM wallet. Switch to Base before signing.

import { createPublicClient, createWalletClient, custom, http } from 'viem';
import { base } from 'viem/chains';
import { toClientEvmSigner } from '@x402/evm';
import { registerExactEvmScheme } from '@x402/evm/exact/client';

async function createBaseSigner(provider) {
  await provider.request({
    method: 'wallet_switchEthereumChain',
    params: [{ chainId: '0x2105' }]
  });

  const walletClient = createWalletClient({ chain: base, transport: custom(provider) });
  const [account] = await walletClient.requestAddresses();
  const publicClient = createPublicClient({ chain: base, transport: http('<https://mainnet.base.org>') });

  return toClientEvmSigner(
    {
      address: account,
      signTypedData: (message) => walletClient.signTypedData({ account, ...message })
    },
    publicClient
  );
}

const baseSigner = await createBaseSigner(window.ethereum);
const quote = await quoteChat(request);
await streamPaidChat({
  quote,
  request,
  registerScheme: (client) =>
    registerExactEvmScheme(client, {
      signer: baseSigner,
      networks: ['eip155:8453'],
      schemeOptions: { 8453: { rpcUrl: '<https://mainnet.base.org>' } }
    })
});

Response Format

Successful paid chat responses stream Server-Sent Events:

data: {"Content":{"choices":[{"delta":{"content":"Hello"}}]}}

data: {"Usage":{"usage":{"prompt_tokens":5,"completion_tokens":10,"total_tokens":15}}}

data: {"Verification":{"verified":true}}

data: [DONE]

Handle at least these event types: