Pay per request with x402
x402 is an open protocol for paying over HTTP. The server answers an unpaid request with 402 Payment Required and a quote, the client signs a payment for that quote and sends the request again, and the server replies and collects. Katara takes x402 on POST https://api.katara.com/v1/chat/completions, POST https://api.katara.com/v1/messages, POST https://api.katara.com/v1/responses, POST https://api.katara.com/v1/embeddings and the two image routes, POST https://api.katara.com/v1/images/generations and POST https://api.katara.com/v1/images/edits: a request with no Authorization header is quoted in USDC on Avalanche C-Chain, and the wallet that signs pays for that one reply.
Use it when there is no account behind the caller: a script, a batch job, or an agent that holds its own wallet. A program that sends many requests for one organization is usually better served by an API key, which pays from a prepaid balance with no minimum charge.
The flow
Section titled “The flow”- Send the request with no key. Katara picks who will serve it and answers
402with a quote in thePAYMENT-REQUIREDheader. - Sign one of the quote’s entries.
- Send the same request again, byte for byte the same body, with the signed payment in the
PAYMENT-SIGNATUREheader. - Katara checks the signature and your balance, runs the request, and replies. After the reply it settles what the reply cost, never more than you signed.
The paid retry is matched to its quote by the request body. A different body, or a quote that has expired, gets a new 402 instead of a reply.
Choosing a model
Section titled “Choosing a model”A chat completion, Messages or Responses request paid this way may name one of three models. Any other name is served as katara/cortex. Image requests are served by Cortex whatever they name, and embeddings name the embedding model.
model | Answered by | Price per million tokens (input, cached input, output) | Minimum per request |
|---|---|---|---|
katara/cortex | Whichever model Cortex picks for the request | The price of the model that answered | 0.025 USDC |
katara/cortex-genius | The Deep model | 2.40, 0.12 and 12 USDC until 30 November 2026; 4.80, 0.24 and 24 USDC from 1 December 2026 | 0.05 USDC |
katara/cortex-smart | The Light model | 0.36, 0.036 and 3 USDC | 0.01 USDC |
katara/cortex-genius and katara/cortex-smart always try their own model first. If it fails before answering, the request goes to the Standard model under the same payment (When a provider fails), charged at its price with the same minimum: 0.90, 0.09 and 4.50 USDC until 31 December 2026, then 1.80, 0.18 and 9 USDC. Neither name moves to any other model, and Katara Community does not serve them. The quote’s extra.model names the model you asked for, and so do a chat completion’s reply and its stream chunks. A Messages or Responses reply names katara/cortex.
The two names are sold only this way. A request that names one with an API key or from a Katara app is refused with 400 model_requires_x402; keyed requests use katara/cortex. On the Test network no model serves either name, so every reply is the simulated one. It is quoted at the simulated reply’s rate, 1 test USDC per million input tokens and 2 per million output tokens, never below the name’s minimum, and charged that minimum unless the prompt alone costs more at that rate. The Test model list shows both names without pricing.
upto or exact
Section titled “upto or exact”Each quote offers the same maximum in two schemes, upto first. x402 clients pay the first entry they can sign, so a client that knows both pays with upto.
upto | exact | |
|---|---|---|
| What you sign | A Permit2 transfer of at most the quoted amount | An EIP-3009 transfer of exactly the quoted amount |
| What leaves your wallet | The reply’s charge, nothing more | The quoted amount; the settlement contract pays the charge and sends the rest back in the same transaction |
| What the wallet asks you to sign | PermitWitnessTransferFrom under the Permit2 domain: the token, the maximum, the x402 upto proxy as spender, a deadline, and a witness naming Katara’s settlement contract and the address that may settle | TransferWithAuthorization under the token’s domain: your wallet, Katara’s settlement contract, the amount, and a validity window |
| Before the first payment | One approval of Permit2 for USDC | Nothing |
The Permit2 approval
Section titled “The Permit2 approval”upto moves tokens through Permit2, a shared contract at 0x000000000022D473030F116dDEE9F6B43aC78BA3 that many wallets have already approved. A wallet that has not approved it for USDC gets 412 with code permit2_allowance_required, and its quote stays valid. Approve once with an ordinary approve(0x000000000022D473030F116dDEE9F6B43aC78BA3, amount) transaction on the token, which costs a little gas, and every later upto payment is a signature only. Or pay the same quote with exact, which needs no approval.
When a quote lists eip2612GasSponsoring under extensions, the token takes EIP-2612 permits and Katara pays for the approval. A client that can read the chain then signs a permit letting Permit2 spend the quoted maximum and sends it with the payment. Katara submits that permit in the same transaction that settles the reply, so it costs you no gas and is never sent without a charge: a request that fails before delivering submits nothing. The permit covers one payment’s maximum, so a client signs a new one while the approval is short.
USDC on Avalanche C-Chain takes EIP-2612 permits, so quotes offer that extension: a wallet that has never approved Permit2 can still pay with upto, and Katara pays the gas for the approval in the same transaction as the first payment.
The challenge
Section titled “The challenge”PAYMENT-REQUIRED is base64 of a JSON object; the response body carries the same JSON. One for a chat request, with the long values shortened:
{ "x402Version": 2, "error": "payment required", "accepts": [ { "scheme": "upto", "network": "eip155:43114", "asset": "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E", "amount": "25000", "payTo": "0x7f8d1049519ddcb1cb041fb1b0df7a125e96acf3", "maxTimeoutSeconds": 300, "extra": { "name": "USD Coin", "version": "2", "assetTransferMethod": "permit2", "facilitatorAddress": "0x88CcDE2C…0B9338", "ticketHash": "0x0201b666…6d9215", "provider": "0x65B03c44…d7d34C", "model": "katara/cortex", "served": "katara/cortex", "managed": true } }, { "scheme": "exact", "network": "eip155:43114", "asset": "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E", "amount": "25000", "payTo": "0x7f8d1049519ddcb1cb041fb1b0df7a125e96acf3", "maxTimeoutSeconds": 300, "extra": { "name": "USD Coin", "version": "2", "nonce": "0x0201b666…6d9215", "ticketHash": "0x0201b666…6d9215", "provider": "0x65B03c44…d7d34C", "model": "katara/cortex", "served": "katara/cortex", "managed": true } } ]}| Field | What it is |
|---|---|
scheme | How the payment is signed. With exact you sign one amount and Katara never collects more. |
network | The chain as a CAIP-2 id, eip155:43114 for Avalanche C-Chain. |
asset | The USDC contract the payment is for. |
amount | The most this request can cost, in the token’s base units (6 decimals, so 25000 is 0.025). |
payTo | Katara’s settlement contract. It is where the payment goes. |
maxTimeoutSeconds | How long to make the authorization valid. Clients set its end to now plus this. |
extra.name, extra.version | The token’s EIP-712 domain for an exact signature. |
extra.nonce, extra.ticketHash | The job this quote is bound to. An exact client may use it as its authorization nonce; a random nonce works too. |
extra.provider | The address that will be paid for the reply. |
extra.model | The model you are paying for: katara/cortex-genius or katara/cortex-smart when the request named one, otherwise katara/cortex, whatever model a chat, Messages, Responses or image request named; the embedding model for embeddings. |
extra.served | katara/cortex for every chat, Messages, Responses and image request, including one that named katara/cortex-genius or katara/cortex-smart; the embedding model for embeddings. |
extra.managed | Present and true when a vendor’s model, not a provider’s node, will answer. |
An upto entry carries the same fields, without extra.nonce, and two more: extra.assetTransferMethod is permit2, and extra.facilitatorAddress is the address that settles, which your signature names so no one else can.
The amount is the price of the quoted provider or vendor over the request’s bounds: the input as estimated from the prompt, plus max_tokens (max_output_tokens on the Responses API) of output. Without it the quote assumes the longest reply the model allows, so set it. Every quote is at least the minimum charge for the model named: 0.025 USDC for katara/cortex, 0.05 for katara/cortex-genius and 0.01 for katara/cortex-smart.
Signing and sending the payment
Section titled “Signing and sending the payment”An exact payment is an EIP-3009 TransferWithAuthorization signed under the token’s EIP-712 domain (extra.name, extra.version, the chain id from network, and asset as the verifying contract):
| Authorization field | Value |
|---|---|
from | Your wallet. |
to | payTo. |
value | amount, exactly. |
validAfter | 0, or a time in the past. |
validBefore | At least a minute away; now plus maxTimeoutSeconds is what clients use. |
nonce | 32 bytes, random or extra.nonce. |
An upto payment is a Permit2 PermitWitnessTransferFrom signed under the domain { name: "Permit2", chainId, verifyingContract: 0x000000000022D473030F116dDEE9F6B43aC78BA3 }:
| Field | Value |
|---|---|
permitted.token | asset. |
permitted.amount | amount, exactly. Katara collects the reply’s charge, up to this. |
spender | The x402 upto proxy, 0x4020A4f3b7b90ccA423B9fabCc0CE57C6C240002. |
nonce | A random 256-bit number. |
deadline | At least a minute away; now plus maxTimeoutSeconds is what clients use. |
witness.to | payTo. |
witness.facilitator | extra.facilitatorAddress. |
witness.validAfter | A time in the past. |
The payload carries payload.signature and payload.permit2Authorization, which holds from and the fields above.
Send the payment as base64 of the x402 payment payload (x402Version, payload, and the accepted entry you chose) in PAYMENT-SIGNATURE. Clients that speak the first version of the protocol send the same payload in X-PAYMENT, which Katara also reads and treats as exact. You do not need gas: Katara submits the settlement.
Before running the request Katara checks that the signature recovers to your wallet, that the wallet holds at least the signed amount, and that the nonce has not been used. A failed check answers 402 with code payment_invalid and leaves the quote in place, so a corrected payment for the same body can still use it.
For upto it also checks that Permit2 may spend the amount, or that the payment carries a permit for it; otherwise the answer is 412 permit2_allowance_required, again with the quote kept.
Settlement and the reply
Section titled “Settlement and the reply”A paid reply carries PAYMENT-RESPONSE, base64 of a JSON object, and the same value in X-PAYMENT-RESPONSE:
{ "success": true, "payer": "0xYourWallet", "transaction": "", "network": "eip155:43114", "amount": "25000" }success means the payment was accepted. amount is what you are charged, in base units. transaction is empty because settlement is submitted after the reply, never before it; the settlement is public on https://snowtrace.io.
You are charged what the reply cost, or the model’s minimum when it cost less, and never more than you signed. With exact the settlement contract pulls the signed amount, pays the provider or vendor, and refunds the difference to your wallet in the same transaction. A reply that fails before anything is delivered is not settled, and its authorization expires unused.
With upto only the charge leaves your wallet, pulled through Permit2.
The reply’s katara object says what was charged and who was paid, as described in Usage and cost:
"katara": { "cost_usdc": "0.025", "payment": { "scheme": "exact", "network": "eip155:43114", "asset": "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E", "amount": "25000" }, "provider": "0x65B03c44…d7d34C"}cost_usdc is the charge in USDC, the minimum included. payment.amount is the same charge in the paid token’s base units, equal to amount in PAYMENT-RESPONSE, and payment.scheme is the scheme you paid with.
Streaming
Section titled “Streaming”With "stream": true the reply is server-sent events, as for a keyed request. PAYMENT-RESPONSE arrives with the response headers, before the first token, so it has no amount. The final chunk before data: [DONE] carries the katara object, whose cost_usdc and payment.amount are the charge. A stream that breaks after content was sent ends with an error chunk and is not charged.
The Responses API
Section titled “The Responses API”POST https://api.katara.com/v1/responses takes the Responses request shape and is quoted, paid and settled exactly as a chat completion: the same challenge, the same schemes, the same minimum and the same settlement. Send the same body again with the payment, as above. max_output_tokens bounds the reply and the quote, so set it.
A response paid this way is stored for 30 days unless the request sends store: false. No account or payer address is kept with it: whoever holds its id can read it, delete it, or continue from it with previous_response_id, so keep the id to yourself. conversation needs a key, and so does web search: a web_search tool on a paid request without a key is answered without searching. Responses lists what the route takes.
When a provider fails
Section titled “When a provider fails”If the model in a Cortex quote fails before delivering anything, or does not start answering in time, Katara sends the same request to the next model under the payment you already signed. You get one reply and one charge, at the price of the model that answered and never more than you signed. A sponsored Permit2 approval goes out with that settlement. When no model answers, nothing is charged and the reply is an error, usually 503 no_capacity.
The next model needs to fit inside your payment. A model that costs more than you signed is passed over, and the models after it are tried. When none of those answers, the reply is a fresh 402 quoting the first model that was passed over. When too little of the authorization’s validity is left to try another model, the fresh 402 quotes the next one. That quote never names the model that failed.
A Community provider gets at least 60 seconds to start answering before it counts as failed, more for a long prompt, and a vendor model up to two minutes for a long prompt, so give a paid request a client timeout of a few minutes.
Treat every 402 that carries PAYMENT-REQUIRED as a new quote, including one that answers a paid request. Before signing, check that network, asset and payTo are the ones in the table below and that amount is no more than you are willing to pay. Bound the number of rounds: three is enough for an ordinary request. A 402 without PAYMENT-REQUIRED is an error with a code; read it rather than signing again. A client that handles only one challenge per request returns the second 402 instead of completing the fallback.
Limits
Section titled “Limits”- Only those routes take x402. Model lists and everything else are free or need a key. Embeddings are quoted for the longest inputs the model takes and settled for the tokens counted.
- Each paid request settles on its own, at a minimum of 0.025 USDC (0.05 for
katara/cortex-genius, 0.01 forkatara/cortex-smart)./v1/modelslists both names withbilling_methods: ["x402"], their first model’s price aspricingwithbasis: model, and theirminimum_chargein base units. Under/test/v1the rows have nopricing.GET /v1/models/{id}answers with thekatara/cortexrow for every id, these two included, so read the list. - Quotes cost Katara work before anyone pays, so each address may ask for 10 unpaid quotes at once and 30 a minute after that, and hold 16 quotes it has not paid. Past either limit the answer is
429with codequote_rate_limitedand aRetry-Afterheader. A quote is good for up to two minutes, and paying it frees its place. A payment for a body with no open quote counts as a request for a new one. - Paying per request without a key works on both networks. Under
https://api.katara.com/test/v1the quote is in test USDC on Avalanche Fuji, which has no value. See Networks for the Test contracts and the faucet. - A wallet has no account, so features that read account settings or account data do not apply. The
katara.knowledgeflag of Cortex is ignored on a paid request.katara.taskworks, and"katara": { "community": false }keeps the request off Katara Community. - A Cortex conversation paid this way is recognised by its first message alone.
Networks and tokens
Section titled “Networks and tokens”| Live | |
|---|---|
| Chain | Avalanche C-Chain |
network | eip155:43114 |
| Token | USDC |
asset | 0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E |
payTo | 0x7f8d1049519ddcb1cb041fb1b0df7a125e96acf3 |
| EIP-712 domain | USD Coin, version 2 |
upto uses Permit2 at 0x000000000022D473030F116dDEE9F6B43aC78BA3 and the x402 upto proxy at 0x4020A4f3b7b90ccA423B9fabCc0CE57C6C240002.
Test takes x402 for model calls too. Send the same requests to https://api.katara.com/test/v1: the quote names eip155:43113 and test USDC, and offers upto and exact as on Live. When no Test provider is free, Cortex answers with a simulated reply, still quoted and paid in test USDC, and its katara object says "simulated": true. test USDC takes no EIP-2612 permit, so upto needs the wallet’s own Permit2 approval first; exact needs none. A run started with a Test key can be paid with x402 in test USDC as well: see Pay for an agent run.
Try it with curl
Section titled “Try it with curl”Ask for a quote without paying, and decode it:
curl -s -D headers.txt -o /dev/null https://api.katara.com/v1/chat/completions -H "Content-Type: application/json" -d '{"model":"katara/cortex","messages":[{"role":"user","content":"hi"}],"max_tokens":8}'grep -i '^payment-required:' headers.txt | cut -d' ' -f2 | tr -d '\r' | base64 -dThe first line prints nothing; headers.txt holds HTTP/2 402 and the header. The second prints the challenge JSON shown above. Signing needs a wallet library, so the paid step is in the examples below.
TypeScript
Section titled “TypeScript”The x402 project’s client packages sign and encode the payment. Install @x402/core, @x402/evm and viem.
The loop sends the request, signs each challenge that comes back, and stops after three rounds. The policy refuses any quote that is not for Katara’s token and settlement contract or that is above your cap; the spend control allows the token and sets the same cap in base units. The client registers both schemes and pays the first entry it can sign.
import { x402Client, x402HTTPClient } from "@x402/core/client";import { ExactEvmScheme } from "@x402/evm";import { UptoEvmScheme } from "@x402/evm/upto/client";import { privateKeyToAccount } from "viem/accounts";
const API = "https://api.katara.com/v1/chat/completions";const NETWORK = "eip155:43114";const USDC = "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E";const PAY_TO = "0x7f8d1049519ddcb1cb041fb1b0df7a125e96acf3";const MAX_AMOUNT = 1_000_000n; // 1 USDC
const account = privateKeyToAccount(process.env.WALLET_PRIVATE_KEY as `0x${string}`);const client = x402Client.fromConfig({ schemes: [ { network: NETWORK, client: new UptoEvmScheme(account) }, { network: NETWORK, client: new ExactEvmScheme(account) }, ], spendControls: { allowedAssets: [{ network: NETWORK, asset: USDC, maxAmountPerPayment: MAX_AMOUNT.toString() }], }, policies: [ (_version, accepts) => accepts.filter( (a) => (a.scheme === "upto" || a.scheme === "exact") && a.network === NETWORK && a.asset.toLowerCase() === USDC.toLowerCase() && a.payTo.toLowerCase() === PAY_TO.toLowerCase() && BigInt(a.amount) <= MAX_AMOUNT, ), ],});const http = new x402HTTPClient(client);
const body = JSON.stringify({ model: "katara/cortex", messages: [{ role: "user", content: "Explain a Merkle tree in two sentences." }], max_tokens: 200,});
let headers: Record<string, string> = { "Content-Type": "application/json" };let res = await fetch(API, { method: "POST", headers, body });for (let attempt = 0; res.status === 402 && attempt < 3; attempt++) { if (!res.headers.get("PAYMENT-REQUIRED")) throw new Error(await res.text()); const required = http.getPaymentRequiredResponse((name) => res.headers.get(name)); const payment = await http.createPaymentPayload(required); headers = { "Content-Type": "application/json", ...http.encodePaymentSignatureHeader(payment) }; res = await fetch(API, { method: "POST", headers, body });}if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const reply = await res.json();console.log(reply.choices[0].message.content);console.log(reply.katara.cost_usdc, http.getPaymentSettleResponse((name) => res.headers.get(name)));createPaymentPayload throws when the policy or the spend control leaves no quote to sign. The package’s wrapFetchWithPayment does the same exchange for one challenge, so it returns the second 402 of a Cortex fallback instead of signing it. For the Responses API, post to https://api.katara.com/v1/responses with a Responses body, such as { model, input, max_output_tokens }, and read reply.output.
Python
Section titled “Python”Install the x402 package with its EVM and httpx extras: pip install "x402[evm,httpx]".
import jsonimport os
import httpxfrom eth_account import Accountfrom x402 import x402ClientSyncfrom x402.http import x402HTTPClientSyncfrom x402.mechanisms.evm import EthAccountSignerfrom x402.mechanisms.evm.exact.register import register_exact_evm_clientfrom x402.mechanisms.evm.upto import UptoEvmScheme
API = "https://api.katara.com/v1/chat/completions"NETWORK = "eip155:43114"USDC = "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E"PAY_TO = "0x7f8d1049519ddcb1cb041fb1b0df7a125e96acf3"MAX_AMOUNT = 1_000_000 # 1 USDC
def katara_only(version, accepts): return [ a for a in accepts if a.scheme in ("upto", "exact") and a.network == NETWORK and a.asset.lower() == USDC.lower() and a.pay_to.lower() == PAY_TO.lower() and int(a.amount) <= MAX_AMOUNT ]
client = x402ClientSync()client.set_spend_controls({ "allowed_assets": [{"network": NETWORK, "asset": USDC, "max_amount_per_payment": str(MAX_AMOUNT)}],})account = Account.from_key(os.environ["WALLET_PRIVATE_KEY"])signer = EthAccountSigner(account)register_exact_evm_client(client, signer, networks=NETWORK, policies=[katara_only])client.register(NETWORK, UptoEvmScheme(signer))payments = x402HTTPClientSync(client)
body = json.dumps({ "model": "katara/cortex", "messages": [{"role": "user", "content": "Explain a Merkle tree in two sentences."}], "max_tokens": 200,})
with httpx.Client(timeout=120) as http: headers = {"Content-Type": "application/json"} res = http.post(API, content=body, headers=headers) for _ in range(3): if res.status_code != 402: break if "PAYMENT-REQUIRED" not in res.headers: raise RuntimeError(res.text) required = payments.get_payment_required_response(res.headers.get) payment = payments.create_payment_payload(required) headers = {"Content-Type": "application/json", **payments.encode_payment_signature_header(payment)} res = http.post(API, content=body, headers=headers) res.raise_for_status()
reply = res.json()print(reply["choices"][0]["message"]["content"])print(reply["katara"]["cost_usdc"], payments.get_payment_settle_response(res.headers.get))Both examples send the body as one fixed string, so every round carries the same bytes. Keep the private key in the environment or a keystore, never in the code.
Pay for an agent run
Section titled “Pay for an agent run”An organization can pay for each Synapse or Locus run with x402 instead of credits or a wallet budget. The run is still started with the organization’s API key, and every grant, approval, policy and budget limit applies; x402 is only what pays.
Three things set it up, once:
- A budget paid per run. In the portal, Locus → Budgets & Grants → Fund a Budget, set Pay with to x402, per run. Its amount is the most one run may cost, in USDC, up to 10 USDC, and its category caps apply to each run. Through the API, create the payer consent with
funding_sourceset tox402and fund it as usual. Nothing is held. - A grant on that budget, as for any run.
- A key allowed to pay with x402. It is off for every key until an owner turns it on: select Allow x402 payment while creating a key that has the
synapse:runs:createpermission, or later on the key under API Keys, and list the wallets that may pay, one per line. Letter case does not matter: the portal shows each address checksummed, and a wallet listed twice counts once. If the key is created but the setting cannot be saved, the dialog that shows the key says so and offers a retry. With no list, any wallet may pay; a list keeps a leaked key from running your agents on someone else’s money. Through the API,setKeyX402/set_key_x402changes it, signed in as an owner; a key cannot change it for itself.
Then start the run as usual, with POST /katara/v1/orgs/{org_id}/environments/{environment_id}/runs and the key. Without a payment the request is answered 402 with the run’s challenge in PAYMENT-REQUIRED and in the body. It offers upto first and exact, both for the run’s maximum, and is bound to that request body for five minutes. Sign one and send the same request again, with the same Idempotency-Key, and the payment in PAYMENT-SIGNATURE. The answer is the run’s 202.
- What is collected. When the run ends, its payment is settled for what the run was charged, rounded down to a whole USDC base unit, never more than the maximum and only once. With
uptoexactly that amount leaves the wallet; withexactthe maximum is pulled and the rest goes back in the same transaction. A run that cost nothing is not settled at all. - How long a run may take. At most one hour, and it stops ten minutes before the authorization’s deadline so that there is time to settle. A run that reaches that point stops with
time_limit, and what it used is settled. The challenge asks for an authorization that lasts long enough. - One payment, one run. Sending the same payment again returns the run it started, not another.
- One run at a time per budget. A budget paid per run serves one paid run at a time. Run several at once on several budgets.
- The wallet covers what it signed. A run is admitted only while the paying wallet’s USDC covers this run’s maximum and every other run it is paying for that has not settled, and one wallet pays for at most three runs at a time.
- In a Test environment. A run started with a Test key is paid in test USDC on Avalanche Fuji: the challenge names
eip155:43113and the asset0xfed854555faeeca02acdb3fed90b0cd4048d3f45, and offersuptoandexactas in Live. test USDC takes no EIP-2612 permit, souptoneeds the wallet’s own Permit2 approval first. Test funds never pay for a Live run, and a Test run is never paid in USDC. - A failed settlement holds the wallet. If a run’s payment cannot be settled, for example because the wallet moved its USDC or revoked its approval, the run still counts as used and the wallet pays for no new run for 24 hours, under any key or organization. The receipt shows the failure and when the hold ends.
Schedules and event sources never pay with x402. A run they start uses its budget’s credits or wallet, and a budget paid per run cannot be used by them.
A run’s charges, runCharges / run_charges, carry the receipt under x402: the paying wallet, the organization and key that started the run, the scheme, what was signed for, what was collected and the transaction, or why settlement failed and until when the wallet is held. The run page in the portal shows the same.
From TypeScript
Section titled “From TypeScript”Give the product client a fetch wrapped by the x402 client. It pays the run’s challenge and sends the request again. The client refuses any payment above $1 unless told otherwise, so raise its cap to your budget’s maximum.
import { ProductClient } from "@katara/platform/products";import { wrapFetchWithPayment, x402Client } from "@x402/fetch";import { ExactEvmScheme, toClientEvmSigner } from "@x402/evm";import { UptoEvmScheme } from "@x402/evm/upto/client";import { privateKeyToAccount } from "viem/accounts";
const NETWORK = "eip155:43114";const account = privateKeyToAccount(process.env.WALLET_PRIVATE_KEY as `0x${string}`);const signer = toClientEvmSigner({ address: account.address, signTypedData: (msg) => account.signTypedData(msg) });
const payments = new x402Client();payments.register(NETWORK, new UptoEvmScheme(signer)); // pays only what the run uses; needs a Permit2 approval oncepayments.register(NETWORK, new ExactEvmScheme(signer));payments.setSpendControls({ allowedAssets: [{ network: NETWORK, asset: "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E", maxAmountPerPayment: "10000000" }] });
const client = new ProductClient({ baseUrl: "https://platform.katara.com", credential: () => process.env.KATARA_API_KEY!, fetch: wrapFetchWithPayment(fetch, payments),});const run = await client.createRun(scope, runRequest, crypto.randomUUID());upto takes the payment through Permit2, which the wallet approves once to spend its USDC. A wallet without that approval can pay with exact.
From Python
Section titled “From Python”create_run_with_x402 sends the run request, hands the challenge to the x402 package and sends the request once more with the payment. Install x402[evm].
import osimport uuid
from eth_account import Accountfrom katara_platform import ProductClientfrom x402 import x402ClientSyncfrom x402.http import x402HTTPClientSyncfrom x402.mechanisms.evm.exact import ExactEvmSchemefrom x402.mechanisms.evm.signers import EthAccountSignerfrom x402.mechanisms.evm.upto import UptoEvmScheme
NETWORK = "eip155:43114"signer = EthAccountSigner(Account.from_key(os.environ["WALLET_PRIVATE_KEY"]))payments = x402ClientSync()payments.register(NETWORK, UptoEvmScheme(signer))payments.register(NETWORK, ExactEvmScheme(signer))payments.set_spend_controls({"allowed_assets": [{"network": NETWORK, "asset": "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E", "max_amount_per_payment": "10000000"}]})http402 = x402HTTPClientSync(payments)
with ProductClient("https://platform.katara.com", lambda: os.environ["KATARA_API_KEY"]) as client: run = client.create_run_with_x402(org, environment, run_request, str(uuid.uuid4()), lambda headers, body, url: http402.handle_402_response(headers, body, url)[0])Run errors
Section titled “Run errors”| HTTP | code | Meaning |
|---|---|---|
| 402 | none, PAYMENT-REQUIRED set | The run’s challenge. Sign it and send the request again. |
| 402 | payment_invalid | The payment did not verify, its challenge expired, it answers another request, or its authorization ends too soon for the run to do anything. Send the request without a payment for a new challenge. |
| 402 | x402_insufficient_funds | The wallet’s USDC does not cover this run with the other runs it is paying for. |
| 402 | x402_wallet_settlement_failed | A run this wallet paid for could not be settled, so the wallet pays for no new run, under any key or organization, for 24 hours after that failure. The message gives the time it can pay again. Pay with another wallet or use credits. |
| 403 | x402_key_required | The run was not started with an API key. |
| 403 | x402_not_enabled | x402 payment is off for the key, or the wallet is not one the key lists. |
| 409 | x402_budget_in_use | Another paid run is using the budget. |
| 409 | x402_payment_required | The budget is paid per run; a schedule or an event source cannot use it. |
| 409 | x402_no_paid_run | Something tried to spend from the budget outside the paid run that holds it, or after that run ended. Only a run started on the budget with a payment spends from it. |
| 429 | x402_wallet_run_limit | The wallet is already paying for three runs. |
| 503 | x402_unavailable | x402 for runs is not available right now. Nothing was charged. |
Errors
Section titled “Errors”| HTTP | code | Meaning |
|---|---|---|
| 402 | none, PAYMENT-REQUIRED set | A quote. Sign it and retry. |
| 402 | payment_invalid | The payment did not verify: wrong amount or recipient, a scheme or asset that was not quoted, a bad signature, an authorization that expires within a minute, a balance below the signed amount, a nonce already used, or a quote another request already used. The message says which. |
| 400 | invalid_request | The payment header is not base64 JSON. |
| 400 | unsupported_parameter | A Responses API call used a field this route does not take. param names it. |
| 401 | invalid_api_key | The request carries a key that is not valid. |
| 429 | quote_rate_limited | Too many unpaid quotes from your address. Wait for Retry-After. |
| 500 | provider_failed | The vendor quoted for an embeddings or image request failed before delivering. Nothing was charged. A chat, Messages or Responses request moves to the next model instead (When a provider fails). |
| 503 | no_capacity | Nobody can serve the request now. |
An upto payment from a wallet that has not approved Permit2, and carries no permit, answers 412 with code permit2_allowance_required. Approve once, or pay the same quote with exact.
The rest are the same as for a keyed request; see Errors.