<aside>
:ambient-logo-original: Ambient Documentation
🌐 Homepage
:discord-symbol-blurple: Discord
:github-mark-white-bg: github.com/ambient-xyz
:x-logo-black: @ambient_xyz
</aside>
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.
JumpGate accepts USDC on:
solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpeip155:8453USDC token addresses:
EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913Use Solana when you want lower network fees. Use Base when your application already has EVM wallet infrastructure.
Before making a paid request, your client needs:
fetch, readable response streams, and the wallet signing APIs you plan to use.The x402 protocol uses the HTTP 402 Payment Required response to negotiate payment for a normal HTTP request.
For JumpGate:
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.
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.
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.
The quote response returns the pricing headers required by /paid/chat/v2:
x402-input-tokens: chat-tokenized input countx402-max-completion-tokens: requested maximum output tokensx402-model-tier: model pricing tier, such as standard or minix402-max-tool-calls: maximum billable tool calls reserved for the requestDo 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.
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);
}
}
}
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 })
});
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>' } }
})
});
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:
Content: streamed model outputUsage: token usage and verification metadata when requestedVerification: payment or inference verification status when requested