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 on-chain, 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 on-chain, 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 on-chain 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>, the first available chat 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 | What the request costs. This is the charge, in units, USD and USDF. |
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. A quote is never below $0.01, which covers the gas of settling it.
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
// on-chain, 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>" },
"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 | The quote. This is what was charged. |
usage_cost | The metered cost of what the request used. |
payment | tx, explorer, payer, amount, 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. The quote is the charge: the difference from the metered cost is not returned.
The payment settles before the model runs. What happens when something then 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) | $0.01 is kept to cover settlement gas. The rest is refunded. |
| You disconnect from a stream before any output | The payment is kept. |
An MCP chat call is cut at its deadline after the provider accepted it, with nothing produced | $0.01 is kept. The rest is refunded. |
| The answer is cut short after output started | The payment stands. The text so far is returned, and 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 | The payment stands, because the provider has billed it. 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
// on-chain, 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 |
|---|---|---|---|
| Loading… | |||
Over MCP, keyless chat shares these limits. See Pay for the live quote widget.