Get started
Pay per request with x402.
No account and no key. Ask for a quote, sign one payment, and send the same request again. Every model endpoint works this way: chat, completions, embeddings, images, audio, rerank and video.
On this page
A request with no credentials is answered with a quote. You sign one payment and send the same request again. The gateway settles the payment onchain, then serves the request.
You need a wallet on Robinhood Chain that holds USDF or USDG. No USDF yet? Buy it on the Buy page, or swap any token for it. Paying in USDF also takes a little ETH, once, for the approval below.
| Step | You send | The gateway answers |
|---|---|---|
| 1. Ask | The request, unpaid | 402 with the quote and two accepted payments |
| 2. Sign | Nothing yet: you sign one payment locally | Nothing. Signing costs no gas |
| 3. Send again | The same request, plus PAYMENT-SIGNATURE | Settles the payment onchain, then serves |
| 4. Read | Nothing | PAYMENT-RESPONSE and a receipt with the answer |
Two assets are accepted for the same amount. USDF is listed first.
| Asset | Method | One-time setup | What you sign |
|---|---|---|---|
| USDF | Permit2 witness transfer | Approve USDF to Permit2 once, in one onchain transaction | EIP-712 PermitWitnessTransferFrom |
| USDG | EIP-3009 | None | EIP-712 TransferWithAuthorization |
- Each payment costs the payer no gas. The gateway submits the transfer.
- Paying in USDF needs a one-time approval to Permit2. That approval is the only transaction a payer sends, and it needs a little ETH for gas. Paying in USDG needs neither.
- An unpaid quote costs nothing.
- A request that fails validation or cannot be routed is refused before any payment.
Samples use <model>.
Send the request exactly as the keyed endpoint takes it, to the same path under /x402, with no credentials.
Ask for a quote
curl -i https://api.usdf.fi/x402/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "<model>",
"messages": [{"role": "user", "content": "Hello"}],
"max_tokens": 64
}'Response, headers then body
402 Payment Required
payment-required: <base64 JSON of the "x402" object below>
www-authenticate: Payment id="…", realm="…", method="evm",
intent="charge", request="…", expires="…"
{
"type": "https://paymentauth.org/problems/payment-required",
"title": "Payment Required",
"status": 402,
"detail": "payment required",
"error": { "message": "payment required", "type": "payment_required", "code": "x402" },
"quote": {
"model": "<model>",
"max_tokens": 64,
"price_version": "<price sheet>",
"amount": { "units": "<units>", "usd": "<usd>", "usdf": "<usdf>" },
"asset": "<USDF address>",
"accepted": [
{ "asset": "<USDF address>", "symbol": "USDF", "method": "permit2", "note": "…" },
{ "asset": "<USDG address>", "symbol": "USDG", "method": "eip3009", "note": "…" }
]
},
"x402": {
"x402Version": 2,
"resource": { "url": "…", "description": "…", "mimeType": "application/json" },
"accepts": [
{
"scheme": "exact", "network": "eip155:<chain-id>", "amount": "<units>",
"asset": "<USDF address>", "payTo": "<payTo>",
"maxTimeoutSeconds": 300, "extra": { "assetTransferMethod": "permit2" }
},
{
"scheme": "exact", "network": "eip155:<chain-id>", "amount": "<units>",
"asset": "<USDG address>", "payTo": "<payTo>",
"maxTimeoutSeconds": 300, "extra": { "name": "Global Dollar", "version": "1" }
}
]
}
}The fields that matter when you pay:
| Field | Meaning |
|---|---|
quote.amount | The most the request can cost, in units, USD and USDF. This is what you pay up front. |
quote.price_version | The price sheet the quote was made from. |
quote.accepted | The two payments, by asset and method. |
quote.units_bound | On non-chat endpoints: how many units the quote covers, and in what. |
accepts[].payTo | The gateway's wallet the payment goes to. |
accepts[].amount | The same amount, in base units. |
accepts[].maxTimeoutSeconds | How long a signed payment may stay valid. |
accepts[].extra | assetTransferMethod: "permit2" for USDF; the EIP-712 name and version for USDG. |
The quote is the most the request can cost: the prompt at one token per UTF-8 byte, the output at max_tokens. The request is charged what it used at that price, and the rest comes back to you. There is no minimum, and the gateway pays the gas.
Once per wallet, approve USDF to Permit2 with an ordinary approve. That is the only transaction a payer ever sends, and it needs a little ETH on Robinhood Chain for gas. Read the allowance first.
Each payment is an EIP-712 signature over a Permit2 witness transfer:
| Part | Value |
|---|---|
| Domain | { name: "Permit2", chainId: <chain id>, verifyingContract: Permit2 } |
| Primary type | PermitWitnessTransferFrom |
| Types | PermitWitnessTransferFrom(TokenPermissions permitted, address spender, uint256 nonce, uint256 deadline, Witness witness), TokenPermissions(address token, uint256 amount), Witness(address to, uint256 validAfter) |
permitted | { token: USDF, amount } |
spender | The x402 exact proxy |
nonce | A random uint256 |
deadline | Now plus maxTimeoutSeconds |
witness | { to: payTo, validAfter: 0 } |
Both addresses are fixed. Permit2's is also in each MPP challenge, as methodDetails.permit2Address. The proxy is the x402 exact scheme's fixed spender. @usdf/sdk exports both, as PERMIT2_ADDRESS and X402_EXACT_PERMIT2_PROXY.
Permit2 0x000000000022D473030F116dDEE9F6B43aC78BA3 x402 exact proxy 0x402085c248EeA27D92E8b30b2C58ed07f9E20001
Send the signed payment as base64 of this JSON in PAYMENT-SIGNATURE:
Payment payload, USDF
{
"x402Version": 2,
"resource": <the resource from the 402>,
"accepted": <the USDF accept, verbatim>,
"payload": {
"signature": "<signature>",
"permit2Authorization": {
"from": "<payer>",
"permitted": { "token": "<USDF address>", "amount": "<units>" },
"spender": "0x402085c248EeA27D92E8b30b2C58ed07f9E20001",
"nonce": "<random uint256, decimal>",
"deadline": "<unix seconds>",
"witness": { "to": "<payTo>", "validAfter": "0" }
}
}
}The SDK reads the allowance and pays in USDF when it is approved, otherwise in USDG.
With the SDK
npm install @usdf/sdk viem
import { USDF } from "@usdf/sdk";
import { privateKeyToAccount } from "viem/accounts";
const wallet = privateKeyToAccount(process.env.PAYER_KEY as `0x${string}`);
const usdf = new USDF({ wallet }); // or new USDF({ apiKey: process.env.USDF_API_KEY })
const result = await usdf.run("<model>", "Hello");The same payment signed by hand, with the x402 client library and viem:
Sign without the SDK
npm install @x402/core@2.27.0 @x402/evm@2.27.0 viem@2.56.8
import { x402Client } from "@x402/core/client";
import {
decodePaymentRequiredHeader,
decodePaymentResponseHeader,
encodePaymentSignatureHeader,
} from "@x402/core/http";
import { createPermit2ApprovalTx, getPermit2AllowanceReadParams } from "@x402/evm";
import { registerExactEvmScheme } from "@x402/evm/exact/client";
import { createPublicClient, createWalletClient, defineChain, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
const url = "https://api.usdf.fi/x402/v1/chat/completions";
const body = JSON.stringify({
model: "<model>",
messages: [{ role: "user", content: "Hello" }],
max_tokens: 256,
});
const headers = { "content-type": "application/json" };
const payer = privateKeyToAccount(process.env.PAYER_KEY as `0x${string}`);
const rhc = defineChain({
id: <chain id>,
name: "Robinhood Chain",
nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
rpcUrls: { default: { http: ["<rpc url>"] } },
});
// Ask without paying: 402, with the accepted payments in the
// PAYMENT-REQUIRED header. The first is USDF through Permit2,
// the second USDG through EIP-3009.
const unpaid = await fetch(url, { method: "POST", headers, body });
const required = decodePaymentRequiredHeader(
unpaid.headers.get("payment-required") ?? "",
);
const usdf = required.accepts[0];
const token = usdf.asset as `0x${string}`;
// Once per wallet: approve USDF to Permit2. This is the only
// transaction the payer ever sends.
const rpc = createPublicClient({ chain: rhc, transport: http() });
const allowance = await rpc.readContract(getPermit2AllowanceReadParams({
tokenAddress: token,
ownerAddress: payer.address,
}));
if (allowance < BigInt(usdf.amount)) {
const approve = createPermit2ApprovalTx(token);
await createWalletClient({ account: payer, chain: rhc, transport: http() })
.sendTransaction({ to: approve.to, data: approve.data });
}
// Allow USDF on this network, then sign. Allowing the USDG accept
// instead (required.accepts[1]) pays by EIP-3009, with no approval.
const client = new x402Client().setSpendControls({
allowedAssets: [{ network: usdf.network, asset: usdf.asset }],
});
registerExactEvmScheme(client, { signer: payer });
const payload = await client.createPaymentPayload(required);
// The same request with the signed permit. The gateway settles it
// onchain, then serves it.
const paid = await fetch(url, {
method: "POST",
headers: {
...headers,
"payment-signature": encodePaymentSignatureHeader(payload),
},
body,
});
const result = await paid.json();
console.log(result.choices[0].message.content);
console.log(
result.receipt.paid.usdf,
result.receipt.payment.asset,
"paid in",
result.receipt.payment.tx,
);
console.log(decodePaymentResponseHeader(
paid.headers.get("payment-response") ?? "",
));USDG needs no approval. Each payment is an EIP-3009 authorization signed as EIP-712 typed data.
| Part | Value |
|---|---|
| Domain | { name: "Global Dollar", version: "1", chainId: <chain id>, verifyingContract: USDG } |
| Primary type | TransferWithAuthorization(address from, address to, uint256 value, uint256 validAfter, uint256 validBefore, bytes32 nonce) |
| Message | { from: payer, to: payTo, value: amount, validAfter: 0, validBefore: now + maxTimeoutSeconds, nonce: random bytes32 } |
| Payload | { signature, authorization: { from, to, value, validAfter, validBefore, nonce } }, with the USDG accept as accepted |
The payment must be valid for at least 30 seconds when it arrives.
Send the identical body again with PAYMENT-SIGNATURE: <encoded payment>. Send a multipart body whole, both times: its quote is read from the file.
Paid request
curl https://api.usdf.fi/x402/v1/chat/completions \
-H "Content-Type: application/json" \
-H "PAYMENT-SIGNATURE: <encoded payment>" \
-d '{
"model": "<model>",
"messages": [{"role": "user", "content": "Hello"}],
"max_tokens": 64
}'Response, trimmed
payment-response: <base64 JSON: success, transaction, network, payer>
x-request-id: <request_id>
{
"id": "<request_id>",
"choices": [ … ],
"receipt": {
"request_id": "<request_id>",
"paid": { "units": "<units>", "usd": "<usd>", "usdf": "<usdf>" },
"usage_cost": { "units": "<units>", "usd": "<usd>", "usdf": "<usdf>" },
"refund": { … },
"payment": {
"tx": "<transaction hash>", "explorer": "…", "payer": "<payer>",
"amount": { "units": "<units>", "usd": "<usd>", "usdf": "<usdf>" },
"asset": "USDF", "method": "permit2"
},
"settlement": { "scheme": "x402 exact", "network": "eip155:<chain-id>" },
"data_policy": { … },
"log": { "proof": "https://api.usdf.fi/v1/log/proof/<request_id>", "note": "…" }
}
}<encoded payment> is the base64 of the payment payload above.
The receipt reports both what you paid and what the request used:
| Field | Meaning |
|---|---|
request_id, model, provider | The request's id, the model without any routing suffix, and the route that served it. |
status | ok, or partial for an answer cut short. |
usage, price | The provider's counts, and the price sheet and rates they were metered at. |
paid | What the request was charged: what it used at the quoted price, never more than the payment. |
usage_cost | The metered cost of what the request used. |
refund | The rest of the payment, sent back in the asset paid, usually within a minute. Null when the request used it all. |
payment | tx, explorer, payer, amount (the quote, paid up front), asset and method of the transfer. |
settlement | scheme is x402 exact, or mpp evm charge for MPP, with the network. |
data_policy | What the route that served the request does with its content, and what the request asked for. |
agent_identity | When the request named an agent with x-erc8004-agent: the identity, as checked. |
log | The request's leaf in the public usage log: its proof URL. |
modality, endpoint, billed | On non-chat endpoints: the modality, the endpoint and the { units, unit } billed. |
lookup | On non-chat endpoints: the public receipt's URL. Every receipt, chat included, is public at GET /x402/v1/receipts/{id}. |
The PAYMENT-RESPONSE header is base64 JSON with the transaction. Whatever the request did not use is refunded to the payer in the asset paid.
The payment settles before the model runs. A served request is charged what it used and the rest is refunded. What happens when something fails:
| Situation | What happens |
|---|---|
| The request fails validation or cannot be routed | Refused before payment. Nothing is taken. |
| The gateway or the provider fails after payment, before any output | Refunded in full, in the asset paid. |
| The provider ends a stream before any output, while you are connected | Refunded in full. |
The provider rejects the request itself (upstream_rejected) | Refunded in full. |
An MCP chat call is cut at its deadline after the provider accepted it, with nothing produced | Refunded in full. |
| You leave a stream | Charged what the provider reports for it. The rest is refunded. |
| The answer is cut short after output started | Charged for the text produced, which is returned, and the rest refunded. The receipt says partial. |
| The model's provider bills reasoning it does not stream, and the request fails or is cut after the provider accepted it | Charged the output budget it held, as the provider bills it, and the rest refunded. The receipt says partial. |
A refund goes back to the payer in the asset paid. The error that reports it names the amount and its transaction, or a reference while it is pending. A pending refund is retried until it lands.
A payment the gateway refuses costs nothing. Each refusal names its reason:
| Code | Status | What to do |
|---|---|---|
permit2_allowance_required | 402 | Approve USDF to Permit2, then pay again after a minute. Paying in USDG is not affected. |
insufficient_funds | 402 | Hold enough of the asset, then pay again after a minute. Paying in the other asset is not affected. |
authorization_used | 402 | Sign a new payment. Each one is single use. |
payment_invalid | 402 | Check the typed data and the accept you signed. |
quote_mismatch | 402 | The price changed. Ask for a new quote. |
settlement_failed | 402 | The transfer did not settle. Nothing was served. Pay again. |
settlement_unconfirmed | 502 | The transfer's outcome is not known yet. Nothing was served. If it lands, it is refunded automatically; the message names the transaction. |
payment_already_resolved | 409 | This payment was already settled another way, for example refunded. Nothing was served. Sign a new payment. |
funds_lease | 503 | Payments pause briefly while gateway instances hand over. Nothing was charged. Retry after Retry-After. |
payment_in_flight | 429 | One payment per payer settles at a time. Retry after Retry-After. |
payments_busy | 429 | The gateway is settling at capacity. Retry after Retry-After. |
rate_limited | 429 | Too many paid requests from this IP address. Retry after Retry-After. |
payment_conflict | 400 | Send an x402 payment or an MPP credential, not both. |
After permit2_allowance_required or insufficient_funds, that address is refused in that asset for one minute, with insufficient_funds and Retry-After. It can still pay in the other asset.
Every model endpoint takes the same payment. Send the body the keyed endpoint takes, with no key, to the same path under /x402.
| Path | Body | Quoted from |
|---|---|---|
POST /x402/v1/chat/completions | Chat | The prompt at one token per byte, the output at max_tokens |
POST /x402/v1/completions | Legacy completions, prompt | The same |
POST /x402/v1/embeddings | model, input | One token per byte of each input |
POST /x402/v1/images/generations | model, prompt, n, size | Image units at the size asked, times n |
POST /x402/v1/images/edits | Multipart, with the image | Image units at the larger of the asked and uploaded size |
POST /x402/v1/audio/transcriptions | Multipart, with the audio file | The longest the file can decode to |
POST /x402/v1/audio/speech | model, input, voice | Characters of input. The body is the audio; the receipt id is in x-request-id |
POST /x402/v1/rerank | model, query or queries, documents | One token per byte of each pair |
POST /x402/v1/videos/generations | model, prompt, seconds | The clip length asked for. Answers 202 with the job and a read_token |
GET /x402/v1/videos/{id} | With Authorization: Bearer <read_token> | The job. Add /content for the clip. No token: 401 read_token_required |
GET /x402/v1/receipts/{id} | None | The public receipt of any request, with no key |
Tools and batch jobs run on a key. A :batch model sent here is refused with batch_unavailable, and nothing is charged.
MPP, the Machine Payments Protocol, is the Payment HTTP authentication scheme. Every /x402 path also answers it. The same 402 carries one WWW-Authenticate: Payment challenge per asset, with method evm and intent charge. The USDG authorization comes first, then USDF through Permit2. A challenge expires after five minutes.
Pay by sending the same request again with Authorization: Payment <credential>. The credential is base64url JSON: challenge (as issued, unchanged), payload, and source as did:pkh:eip155:<chain-id>:<address>. The paid answer carries Payment-Receipt.
A refused credential gets 402 with fresh challenges and a problem type:
| Problem type | Meaning |
|---|---|
malformed-credential | The credential could not be read. |
invalid-challenge | Not issued by this gateway, altered, or already paid. |
payment-expired | The challenge or the signature has expired. |
invalid-payload | The payload does not have the shape its type needs. |
verification-failed | The payment does not match the challenge, or the payer cannot fund it. |
Contract wallets pay through ERC-1271. Over MCP with no key, the credential goes in _meta["org.paymentauth/credential"].
Pay with an MPP credential
npm install viem@2.56.8
import { getAddress, keccak256, stringToHex } from "viem";
import { privateKeyToAccount } from "viem/accounts";
const url = "https://api.usdf.fi/x402/v1/chat/completions";
const body = JSON.stringify({
model: "<model>",
messages: [{ role: "user", content: "Hello" }],
max_tokens: 256,
});
const headers = { "content-type": "application/json" };
const payer = privateKeyToAccount(process.env.PAYER_KEY as `0x${string}`);
const fromB64 = (s: string) =>
JSON.parse(Buffer.from(s, "base64url").toString("utf8"));
// Ask without paying: 402, with one Payment challenge per asset
// in WWW-Authenticate.
const unpaid = await fetch(url, { method: "POST", headers, body });
const challenges = [...(unpaid.headers.get("www-authenticate") ?? "")
.matchAll(/Payment ((?:[a-z]+="(?:[^"\\]|\\.)*"(?:, )?)+)/g)].map((m) =>
Object.fromEntries([...m[1].matchAll(/([a-z]+)="((?:[^"\\]|\\.)*)"/g)]
.map((p) => [p[1], p[2].replace(/\\(.)/g, "$1")])),
);
// The USDG challenge: an EIP-3009 authorization whose nonce is
// keccak256(id || realm), valid until the challenge expires.
const c = challenges.find((x) =>
fromB64(x.request).methodDetails.credentialTypes[0] === "authorization")!;
const request = fromB64(c.request);
const nonce = keccak256(stringToHex(c.id + c.realm));
const validBefore = BigInt(Math.floor(Date.parse(c.expires) / 1000));
const signature = await payer.signTypedData({
domain: {
name: "Global Dollar",
version: "1",
chainId: request.methodDetails.chainId,
verifyingContract: getAddress(request.currency),
},
types: {
TransferWithAuthorization: [
{ name: "from", type: "address" },
{ name: "to", type: "address" },
{ name: "value", type: "uint256" },
{ name: "validAfter", type: "uint256" },
{ name: "validBefore", type: "uint256" },
{ name: "nonce", type: "bytes32" },
],
},
primaryType: "TransferWithAuthorization",
message: {
from: payer.address,
to: getAddress(request.recipient),
value: BigInt(request.amount),
validAfter: 0n,
validBefore,
nonce,
},
});
const credential = {
// The challenge as issued, every parameter (opaque included) unchanged.
challenge: {
id: c.id, realm: c.realm, method: c.method, intent: c.intent,
request: c.request, expires: c.expires, opaque: c.opaque,
...(c.description ? { description: c.description } : {}),
},
payload: {
type: "authorization", from: payer.address, to: request.recipient,
value: request.amount, validAfter: "0",
validBefore: validBefore.toString(), nonce, signature,
},
source: `did:pkh:eip155:${request.methodDetails.chainId}:${payer.address}`,
};
// The same request with the credential. The gateway settles it
// onchain, then serves it.
const paid = await fetch(url, {
method: "POST",
headers: {
...headers,
authorization: `Payment ${Buffer.from(JSON.stringify(credential)).toString("base64url")}`,
},
body,
});
const result = await paid.json();
console.log(result.choices[0].message.content);
console.log(fromB64(paid.headers.get("payment-receipt") ?? ""));A paid request can name the agent behind it. These headers are optional:
| Header | Effect |
|---|---|
x-erc8004-agent | An ERC-8004 agent id, or eip155:<chain-id>:<registry>:<agentId>. The payer must be the identity's owner or its agentWallet. This is checked before payment, at no cost. |
AP2-Mandate | An agent payment mandate. Recorded by its hash on the receipt, not verified. |
Signature-Input, Signature, Signature-Agent | An HTTP message signature credential. Recorded by its key id, not verified. |
A malformed id, or one in another registry, is refused with agent_identity_invalid. An unknown agent id gets agent_identity_unknown, and a payer that is not the identity's gets agent_identity_not_payer. Each costs nothing.
Paid requests have their own limits, read live from GET /v1/limits:
| Applies to | Scope | Limit | Error code |
|---|---|---|---|
Over MCP, keyless chat shares these limits.