# Pay per request with x402 > Call chat completions, the Responses API or embeddings without an account or key, paying for each reply with a signed USDC authorization from your wallet. Section: Developers. Web page: https://docs.katara.com/developers/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/responses` and `POST https://api.katara.com/v1/embeddings`: 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](https://docs.katara.com/start/api.txt), which pays from a prepaid balance with no minimum charge. ## The flow 1. Send the request with no key. Katara picks who will serve it and answers `402` with a quote in the `PAYMENT-REQUIRED` header. 2. Sign one of the quote's entries. 3. Send the same request again, byte for byte the same body, with the signed payment in the `PAYMENT-SIGNATURE` header. 4. 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. ## 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 `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 `PAYMENT-REQUIRED` is base64 of a JSON object; the response body carries the same JSON. One for `katara/cortex@1`, with the long values shortened: ```json { "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": "0xF4b8e060…379793", "ticketHash": "0x0201b666…6d9215", "provider": "0x65B03c44…d7d34C", "model": "katara/cortex@1", "served": "katara/deepseek-chat@1", "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@1", "served": "katara/deepseek-chat@1", "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`, `extra.served` | The model you asked for and the model that will answer. | | `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 0.025 USDC, the [minimum charge](https://docs.katara.com/concepts/pricing.txt#paying-per-request-from-a-wallet) for a request paid this way. ## 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 A paid reply carries `PAYMENT-RESPONSE`, base64 of a JSON object, and the same value in `X-PAYMENT-RESPONSE`: ```json { "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 0.025 USDC 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 served it, as described in [Usage and cost](https://docs.katara.com/developers/usage.txt): ```json "katara": { "cost_usdc": "0.025", "payment": { "scheme": "exact", "network": "eip155:43114", "asset": "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E", "amount": "25000" }, "provider": "0x65B03c44…d7d34C", "served": { "model": "katara/deepseek-chat@1" } } ``` `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 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 `POST https://api.katara.com/v1/responses` takes the OpenAI Responses API 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. Responses calls are stateless, with or without a key. Katara stores no response and keeps no conversation, so these are refused with `400` and code `stateless_only`, before any quote or charge: - `store: true` (`store: false` is accepted, and every reply says `"store": false`), - `previous_response_id`, - `conversation`, - `background: true`, - a stored `prompt`, - an `item_reference` input item. Send the whole conversation in `input` on every call: messages, and the `function_call` and `function_call_output` items of earlier tool turns. Reasoning items from an earlier reply may be sent back and are skipped. | Field | Support | |---|---| | `model`, `input`, `instructions` | `input` is a string or a list of message, `function_call` and `function_call_output` items; message content is text, or images given by `image_url` in a user message. | | `max_output_tokens` | Bounds the reply and the quote. Set it. | | `temperature`, `top_p` | As for chat completions. | | `tools`, `tool_choice`, `parallel_tool_calls` | Function tools only. | | `text.format` | `text`, `json_object` or `json_schema`. | | `reasoning.effort` | For models that reason. | | `stream` | Server-sent Responses events. | | `metadata` | Returned as sent. | | `katara` | As for chat completions. | Any other field, including `include`, built-in tools and file inputs, is refused with `400` and code `unsupported_parameter` naming it in `param`. `user`, `safety_identifier` and `prompt_cache_key` are accepted and not passed on. The reply is a Response object: `output` holds a `message` item with `output_text`, a `reasoning` item when the model reasoned, and a `function_call` item per tool call; `usage` counts input and output tokens; the top-level `katara` object reports the charge. A reply cut at `max_output_tokens` has status `incomplete`. A stream sends `response.created`, the output item and delta events, and ends with `response.completed` (or `response.incomplete`), whose `response.katara` carries the charge; it has no `data: [DONE]`. A stream that breaks after content was sent ends with `response.failed` and is not charged. With a key, `/v1/responses` is paid like a chat completion with that key: from Account Credits or in USDC, and Katara Community can serve it either way unless you turn it off. The stateless rule is the same. ## When a provider fails A payment is bound to the provider or vendor in its quote. If that one fails before delivering anything: - For a named model the reply is `502 provider_failed` and nothing is charged. Request a new quote. - For `katara/cortex@1` the reply is a fresh `402` quoting the next candidate, which is not the one that failed. Sign it and send the request again. When no candidate is left, the answer is `503 no_capacity`. A Community provider gets up to 60 seconds to start answering before it counts as failed, so give a paid request a client timeout longer than that. 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 - Only `POST /v1/chat/completions`, `POST /v1/responses` and `POST /v1/embeddings` take x402. Model lists and everything else are free or need a key. [Embeddings](https://docs.katara.com/developers/embeddings.txt) 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. - 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 `429` with code `quote_rate_limited` and a `Retry-After` header. 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/v1` the quote is in test USDC on Avalanche Fuji, which has no value. See [Networks](https://docs.katara.com/about/networks.txt) 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.knowledge` flag of [Cortex](https://docs.katara.com/concepts/cortex.txt#answers-about-katara) is ignored on a paid request. `katara.task` works, 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 | | 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. 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](#pay-for-an-agent-run). ## Try it with curl Ask for a quote without paying, and decode it: ```sh curl -s -D headers.txt -o /dev/null https://api.katara.com/v1/chat/completions -H "Content-Type: application/json" -d '{"model":"katara/cortex@1","messages":[{"role":"user","content":"hi"}],"max_tokens":8}' grep -i '^payment-required:' headers.txt | cut -d' ' -f2 | tr -d '\r' | base64 -d ``` The 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 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. ```ts 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@1", messages: [{ role: "user", content: "Explain a Merkle tree in two sentences." }], max_tokens: 200, }); let headers: Record = { "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 Install the x402 package with its EVM and httpx extras: `pip install "x402[evm,httpx]"`. ```python import json import os import httpx from eth_account import Account from x402 import x402ClientSync from x402.http import x402HTTPClientSync from x402.mechanisms.evm import EthAccountSigner from x402.mechanisms.evm.exact.register import register_exact_evm_client from 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@1", "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 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: 1. **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_source` set to `x402` and fund it as usual. Nothing is held. 2. **A grant on that budget,** as for any run. 3. **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:create` permission, 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_x402` changes 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 `upto` exactly that amount leaves the wallet; with `exact` the 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:43113` and the asset `0xfed854555faeeca02acdb3fed90b0cd4048d3f45`, and offers `upto` and `exact` as in Live. test USDC takes no EIP-2612 permit, so `upto` needs 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 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. ```ts 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 once payments.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 `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]`. ```python import os import uuid from eth_account import Account from katara_platform import ProductClient from x402 import x402ClientSync from x402.http import x402HTTPClientSync from x402.mechanisms.evm.exact import ExactEvmScheme from x402.mechanisms.evm.signers import EthAccountSigner from 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 | 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 | 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 | `stateless_only` | A Responses API call asked for stored state. `param` names the field. | | 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`. | | 502 | `provider_failed` | The quoted provider of a named model failed before delivering. Nothing was charged. | | 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](https://docs.katara.com/developers/errors.txt).