Pay per request with x402

Pay-per-request inference through JumpGate and the x402 payment protocol. Quote an exact request, pay in USDC on Solana or Base, and stream the response.

x402 lets a client pay for a single inference request with USDC, without an account or prepaid credits. Payments are live today on Solana and Base; the integration is a quote-then-pay flow against two JumpGate endpoints.

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 these trusted USDC rails:

Network Chain identifier USDC token address Trusted recipient
Solana mainnet solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v AYYo37bgztqAgP6wp2S9SiYWcijr8gz2o14c8RGWojJZ
Base mainnet eip155:8453 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 0x6992c688c56be442efe1e1ce7d5c95212ed7d59b

Validate network, asset, recipient, and amount before signing. Normalize Base addresses to lowercase before comparison; preserve case for Solana identifiers. 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:

  • Solana: a Solana wallet with canonical mainnet USDC and access to a Solana mainnet RPC endpoint. The facilitator supplies the fee payer for the current exact-payment flow, so the buyer does not need SOL for the x402 transfer fee.
  • Base: an EVM wallet with canonical USDC on Base mainnet and access to a Base RPC endpoint. The EIP-3009 flow is gas-sponsored, so the buyer does not need ETH for the x402 transfer fee.
  • TypeScript runtime: browser or runtime support for fetch, readable response streams, and the wallet signing APIs you plan to use.

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. The exact package set this page is written against is pinned under TypeScript client.

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:

Header Meaning
x402-input-tokens Chat-tokenized input count
x402-max-completion-tokens Requested maximum output tokens
x402-model-tier Model pricing tier, such as standard or mini
x402-max-tool-calls Maximum billable tool calls reserved for the request

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#

Use Node.js 20.18 or newer and pin the payment and wallet libraries used by the integration:

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. The registerPolicy filter is the local spend cap: it rejects any payment requirement that is not the exact scheme, not a trusted asset and recipient, or above quote.max_amount_micro_usdc.

import { x402Client } from '@x402/core/client';
import { wrapFetchWithPayment } from '@x402/fetch';
 
const TRUSTED_RAILS: Record<string, { asset: string; payTo: string }> = {
  'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp': {
    asset: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
    payTo: 'AYYo37bgztqAgP6wp2S9SiYWcijr8gz2o14c8RGWojJZ'
  },
  'eip155:8453': {
    asset: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913',
    payTo: '0x6992c688c56be442efe1e1ce7d5c95212ed7d59b'
  }
};
 
type PaymentRequirement = {
  network: string;
  scheme: string;
  asset: string;
  payTo: string;
  amount?: string;
  maxAmountRequired?: string;
};
 
function paymentAmountMicroUsdc(requirement: PaymentRequirement) {
  return BigInt(requirement.amount ?? requirement.maxAmountRequired ?? 0);
}
 
function isTrustedRequirement(requirement: PaymentRequirement, maxAmount: bigint) {
  const rail = TRUSTED_RAILS[requirement.network];
  if (!rail || requirement.scheme !== 'exact') return false;
  const normalize = (value: string) =>
    requirement.network.startsWith('eip155:') ? value.toLowerCase() : value;
  const amount = paymentAmountMicroUsdc(requirement);
  return (
    normalize(requirement.asset) === normalize(rail.asset) &&
    normalize(requirement.payTo) === normalize(rail.payTo) &&
    amount > 0n &&
    amount <= maxAmount
  );
}
 
async function streamPaidChat({
  quote,
  request,
  registerScheme
}: {
  quote: { max_amount_micro_usdc?: string; amount_micro_usdc: string };
  request: unknown;
  registerScheme: (client: x402Client) => void;
}) {
  const client = new x402Client();
  const maxAmount = BigInt(quote.max_amount_micro_usdc ?? quote.amount_micro_usdc);
 
  client.registerPolicy((_version: number, requirements: PaymentRequirement[]) =>
    requirements.filter((requirement) => isTrustedRequirement(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';
 
type Eip1193Provider = {
  request: (args: { method: string; params?: unknown[] }) => Promise<unknown>;
};
 
async function createBaseSigner(provider: Eip1193Provider) {
  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:

Event Contents
Content Streamed model output
Usage Token usage and verification metadata when requested
Verification Payment or inference verification status when requested
[DONE] Stream complete

Testing and debugging#

Before sending a real paid request, check that the selected wallet holds enough canonical USDC. Base EIP-3009 and the current Solana exact-payment flow use facilitator-sponsored transaction fees, so the buyer does not need ETH or SOL for the x402 transfer fee.

Solana USDC balance:

spl-token balance EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v --url mainnet-beta

Base USDC balance:

cast call 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 \
  "balanceOf(address)(uint256)" <your-address> \
  --rpc-url https://mainnet.base.org

Common issues:

  • Insufficient funds: add canonical USDC for the payment amount.
  • Wrong network: use Solana mainnet or Base mainnet only; devnet and testnet are not supported.
  • Payment rejected: confirm the wallet, network, canonical asset, trusted recipient, USDC balance, and local max-spend policy.
  • 403 Forbidden: payment was rejected or below the facilitator requirement.
  • 402 Payment Required is returned to your app: confirm the x402 wrapper is installed, the exact Solana or EVM scheme is registered, and the request includes the quote headers.
  • 429 or 503 before authorization: back off and retry the quote or unsigned request.

Production notes#

  • Use a local max spend policy based on quote.max_amount_micro_usdc.
  • Use HTTPS RPC endpoints from a reliable provider for production clients.
  • Keep private keys out of source control and prefer wallet adapters or secure key management.
  • Log payment status, request status, response latency, and selected payment network.
  • Do not reuse quote headers for a different request body.

FAQ#

Do I need an account or API key?#

No. Account-free paid-chat requests need only a compatible wallet with canonical USDC. The facilitator sponsors the transaction fee for the supported Base EIP-3009 and Solana exact-payment flows.

Which networks are supported?#

Solana mainnet and Base mainnet.

Which token is supported?#

USDC only.

Can I use devnet or testnet?#

No. JumpGate x402 payments are supported on mainnet only.

Which endpoint should new clients use?#

Use /paid/chat/v2. /paid/chat may exist for older x402 v1 clients, but it is compatibility-only.

How should I limit spend?#

Call /paid/chat/v2/quote first and enforce a local max spend using quote.max_amount_micro_usdc. Do not estimate pricing locally.

Additional resources#