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:
- Your client sends the paid chat request to JumpGate.
- JumpGate returns x402 payment requirements for Solana and Base.
- Your x402 client selects a supported network and signs the USDC payment.
- The client retries the request with payment proof.
- 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:
npm install --save-exact @x402/[email protected] @x402/[email protected] @x402/[email protected] @x402/[email protected] [email protected] @solana/[email protected] @solana/[email protected]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-betaBase USDC balance:
cast call 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 \
"balanceOf(address)(uint256)" <your-address> \
--rpc-url https://mainnet.base.orgCommon 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 Requiredis 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.429or503before 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#
- x402 protocol: the specification this flow implements
- x402 buyer quickstart: upstream client-side walkthrough
- Ambient billing: human-readable pricing
Related x402 flow#
- Headless x402 subscriptions: create and fund an API-only Ambient account for ongoing inference without a browser login.
- Subscription client example: a worked subscribe-then-call client for that flow.
Related pages#
- Solana quickstart: Ambient's Solana-compatible RPC and SVM tooling
- Models and readiness: what the network is serving right now
- Streaming: SSE mechanics for reading the paid response