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.

—Per caller
—Network
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.

StepYou sendThe gateway answers
1. AskThe request, unpaid402 with the quote and two accepted payments
2. SignNothing yet: you sign one payment locallyNothing. Signing costs no gas
3. Send againThe same request, plus PAYMENT-SIGNATURESettles the payment on-chain, then serves
4. ReadNothingPAYMENT-RESPONSE and a receipt with the answer

Two assets are accepted for the same amount. USDF is listed first.

AssetMethodOne-time setupWhat you sign
USDFPermit2 witness transferApprove USDF to Permit2 once, in one on-chain transactionEIP-712 PermitWitnessTransferFrom
USDGEIP-3009NoneEIP-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
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:

FieldMeaning
quote.amountWhat the request costs. This is the charge, in units, USD and USDF.
quote.price_versionThe price sheet the quote was made from.
quote.acceptedThe two payments, by asset and method.
quote.units_boundOn non-chat endpoints: how many units the quote covers, and in what.
accepts[].payToThe gateway's wallet the payment goes to.
accepts[].amountThe same amount, in base units.
accepts[].maxTimeoutSecondsHow long a signed payment may stay valid.
accepts[].extraassetTransferMethod: "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:

PartValue
Domain{ name: "Permit2", chainId: <chain id>, verifyingContract: Permit2 }
Primary typePermitWitnessTransferFrom
TypesPermitWitnessTransferFrom(TokenPermissions permitted, address spender, uint256 nonce, uint256 deadline, Witness witness), TokenPermissions(address token, uint256 amount), Witness(address to, uint256 validAfter)
permitted{ token: USDF, amount }
spenderThe x402 exact proxy
nonceA random uint256
deadlineNow 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

TypeScript
Install
npm install @usdf/sdk viem
Code
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

TypeScript
Install
npm install @x402/core@2.27.0 @x402/evm@2.27.0 viem@2.56.8
Code
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.

PartValue
Domain{ name: "Global Dollar", version: "1", chainId: <chain id>, verifyingContract: USDG }
Primary typeTransferWithAuthorization(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
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:

FieldMeaning
request_id, model, providerThe request's id, the model without any routing suffix, and the route that served it.
statusok, or partial for an answer cut short.
usage, priceThe provider's counts, and the price sheet and rates they were metered at.
paidThe quote. This is what was charged.
usage_costThe metered cost of what the request used.
paymenttx, explorer, payer, amount, asset and method of the transfer.
settlementscheme is x402 exact, or mpp evm charge for MPP, with the network.
data_policyWhat the route that served the request does with its content, and what the request asked for.
agent_identityWhen the request named an agent with x-erc8004-agent: the identity, as checked.
logThe request's leaf in the public usage log: its proof URL.
modality, endpoint, billedOn non-chat endpoints: the modality, the endpoint and the { units, unit } billed.
lookupOn 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:

SituationWhat happens
The request fails validation or cannot be routedRefused before payment. Nothing is taken.
The gateway or the provider fails after payment, before any outputRefunded in full, in the asset paid.
The provider ends a stream before any output, while you are connectedRefunded 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 outputThe 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 startedThe 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 itThe 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:

CodeStatusWhat to do
permit2_allowance_required402Approve USDF to Permit2, then pay again after a minute. Paying in USDG is not affected.
insufficient_funds402Hold enough of the asset, then pay again after a minute. Paying in the other asset is not affected.
authorization_used402Sign a new payment. Each one is single use.
payment_invalid402Check the typed data and the accept you signed.
quote_mismatch402The price changed. Ask for a new quote.
settlement_failed402The transfer did not settle. Nothing was served. Pay again.
settlement_unconfirmed502The transfer's outcome is not known yet. Nothing was served. If it lands, it is refunded automatically; the message names the transaction.
payment_already_resolved409This payment was already settled another way, for example refunded. Nothing was served. Sign a new payment.
funds_lease503Payments pause briefly while gateway instances hand over. Nothing was charged. Retry after Retry-After.
payment_in_flight429One payment per payer settles at a time. Retry after Retry-After.
payments_busy429The gateway is settling at capacity. Retry after Retry-After.
rate_limited429Too many paid requests from this IP address. Retry after Retry-After.
payment_conflict400Send 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.

Next: errors by area

Every model endpoint takes the same payment. Send the body the keyed endpoint takes, with no key, to the same path under /x402.

PathBodyQuoted from
POST /x402/v1/chat/completionsChatThe prompt at one token per byte, the output at max_tokens
POST /x402/v1/completionsLegacy completions, promptThe same
POST /x402/v1/embeddingsmodel, inputOne token per byte of each input
POST /x402/v1/images/generationsmodel, prompt, n, sizeImage units at the size asked, times n
POST /x402/v1/images/editsMultipart, with the imageImage units at the larger of the asked and uploaded size
POST /x402/v1/audio/transcriptionsMultipart, with the audio fileThe longest the file can decode to
POST /x402/v1/audio/speechmodel, input, voiceCharacters of input. The body is the audio; the receipt id is in x-request-id
POST /x402/v1/rerankmodel, query or queries, documentsOne token per byte of each pair
POST /x402/v1/videos/generationsmodel, prompt, secondsThe 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}NoneThe 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.

Next: every modality's parameters

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 typeMeaning
malformed-credentialThe credential could not be read.
invalid-challengeNot issued by this gateway, altered, or already paid.
payment-expiredThe challenge or the signature has expired.
invalid-payloadThe payload does not have the shape its type needs.
verification-failedThe 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

TypeScript
Install
npm install viem@2.56.8
Code
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:

HeaderEffect
x-erc8004-agentAn 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-MandateAn agent payment mandate. Recorded by its hash on the receipt, not verified.
Signature-Input, Signature, Signature-AgentAn 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 toScopeLimitError code
Loading…

Over MCP, keyless chat shares these limits. See Pay for the live quote widget.