Skip to content

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/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, which pays from a prepaid balance with no minimum charge.

  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.

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.

uptoexact
What you signA Permit2 transfer of at most the quoted amountAn EIP-3009 transfer of exactly the quoted amount
What leaves your walletThe reply’s charge, nothing moreThe quoted amount; the settlement contract pays the charge and sends the rest back in the same transaction
What the wallet asks you to signPermitWitnessTransferFrom 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 settleTransferWithAuthorization under the token’s domain: your wallet, Katara’s settlement contract, the amount, and a validity window
Before the first paymentOne approval of Permit2 for USDCNothing

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.

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:

{
"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
}
}
]
}
FieldWhat it is
schemeHow the payment is signed. With exact you sign one amount and Katara never collects more.
networkThe chain as a CAIP-2 id, eip155:43114 for Avalanche C-Chain.
assetThe USDC contract the payment is for.
amountThe most this request can cost, in the token’s base units (6 decimals, so 25000 is 0.025).
payToKatara’s settlement contract. It is where the payment goes.
maxTimeoutSecondsHow long to make the authorization valid. Clients set its end to now plus this.
extra.name, extra.versionThe token’s EIP-712 domain for an exact signature.
extra.nonce, extra.ticketHashThe job this quote is bound to. An exact client may use it as its authorization nonce; a random nonce works too.
extra.providerThe address that will be paid for the reply.
extra.model, extra.servedThe model you asked for and the model that will answer.
extra.managedPresent 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 for a request paid this way.

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 fieldValue
fromYour wallet.
topayTo.
valueamount, exactly.
validAfter0, or a time in the past.
validBeforeAt least a minute away; now plus maxTimeoutSeconds is what clients use.
nonce32 bytes, random or extra.nonce.

An upto payment is a Permit2 PermitWitnessTransferFrom signed under the domain { name: "Permit2", chainId, verifyingContract: 0x000000000022D473030F116dDEE9F6B43aC78BA3 }:

FieldValue
permitted.tokenasset.
permitted.amountamount, exactly. Katara collects the reply’s charge, up to this.
spenderThe x402 upto proxy, 0x4020A4f3b7b90ccA423B9fabCc0CE57C6C240002.
nonceA random 256-bit number.
deadlineAt least a minute away; now plus maxTimeoutSeconds is what clients use.
witness.topayTo.
witness.facilitatorextra.facilitatorAddress.
witness.validAfterA 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.

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 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:

"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.

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.

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.

FieldSupport
model, input, instructionsinput 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_tokensBounds the reply and the quote. Set it.
temperature, top_pAs for chat completions.
tools, tool_choice, parallel_tool_callsFunction tools only.
text.formattext, json_object or json_schema.
reasoning.effortFor models that reason.
streamServer-sent Responses events.
metadataReturned as sent.
kataraAs 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.

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.

  • 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 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 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 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.
Live
ChainAvalanche C-Chain
networkeip155:43114
TokenUSDC
asset0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E
payTo0x7f8d1049519ddcb1cb041fb1b0df7a125e96acf3
EIP-712 domainUSD 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.

Ask for a quote without paying, and decode it:

Terminal window
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.

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@1",
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.

Install the x402 package with its EVM and httpx extras: pip install "x402[evm,httpx]".

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.

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.

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 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.

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 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])
HTTPcodeMeaning
402none, PAYMENT-REQUIRED setThe run’s challenge. Sign it and send the request again.
402payment_invalidThe 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.
402x402_insufficient_fundsThe wallet’s USDC does not cover this run with the other runs it is paying for.
402x402_wallet_settlement_failedA 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.
403x402_key_requiredThe run was not started with an API key.
403x402_not_enabledx402 payment is off for the key, or the wallet is not one the key lists.
409x402_budget_in_useAnother paid run is using the budget.
409x402_payment_requiredThe budget is paid per run; a schedule or an event source cannot use it.
409x402_no_paid_runSomething 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.
429x402_wallet_run_limitThe wallet is already paying for three runs.
503x402_unavailablex402 for runs is not available right now. Nothing was charged.
HTTPcodeMeaning
402none, PAYMENT-REQUIRED setA quote. Sign it and retry.
402payment_invalidThe 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.
400invalid_requestThe 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.