Payments & x402

Keyless, per-request agent payments in USDC on Base using the x402 HTTP transport

What is x402?

x402 turns HTTP 402 Payment Required into a working payment rail. When an autonomous agent calls a paid Krawl endpoint without paying, the server answers with a machine-readable challenge describing the exact price, asset, and receiving wallet. The agent signs a USDC payment authorization, retries with an X-PAYMENT header, and gets the result.

This matters for agents because it removes every human step:

  • No account, no signup. There is nothing to register.
  • No API key. The payment itself is the authorization.
  • No human in the loop. An agent discovers the price, pays, and gets work done in a single retry, all over plain HTTP.

Payments settle in USDC on Base. Krawl never custodies funds or touches a private key. A facilitator service verifies the signature and broadcasts the on-chain settlement.

Krawl implements x402 specification v1 (HTTP transport). The version constant surfaced in every response is x402Version: 1. Spec: github.com/coinbase/x402.

Pricing

Krawl charges per research request, by tier:

TierPriceAtomic USDC unitsEnv var
Quick krawl (mode=quick)$1.001000000X402_PRICE_SHALLOW
Deep krawl (mode=deep)$5.005000000X402_PRICE_DEEP

USDC has 6 decimals, so prices are expressed in atomic units: 1000000 = $1.00. Both prices are configurable via the Krawl server's environment variables (see Server x402 configuration). The amount advertised in the challenge comes through the maxAmountRequired field as a string.

Which endpoints are gated

x402 gates only the research endpoints. Everything else stays on the normal API-key auth path.

Endpointx402 gated?Notes
POST /researchYesDeep or quick depending on mode
POST /research/uploadYesDeep research with uploaded documents
POST /chat/completions and POST /v1/chat/completionsYesOpenAI-compatible surface
POST /templates/{template_id}/researchYesTemplate execution
POST /research/asyncNoRequires an API key (no keyless x402)
POST /searchNoRequires an API key
GET /results, /templates, /sessions, etc.NoStandard API-key auth only

Self-hosting warning: x402 protects the research endpoints, but the other endpoints (/results, /templates, /sessions, and friends) use regular API-key auth. If you enable x402 without setting KRAWL_DEV_KEY, those other endpoints stay publicly accessible and the server logs a warning on startup. Set KRAWL_DEV_KEY to protect them.

The payment flow

Here is the exact sequence Krawl runs, with the real field names from the implementation.

1. Agent calls a gated endpoint with no payment

The agent POSTs to a research endpoint without an X-PAYMENT header (and without an API key).

curl -i -N -X POST https://api.krawl.sh/research \
  -H "Content-Type: application/json" \
  -d '{"query": "What is EIP-4844?", "mode": "quick"}'

2. Server returns HTTP 402 with a payment-requirements challenge

The server responds 402 Payment Required. The body is rendered at the top level (not nested under a detail key) so standard x402 clients can read accepts and x402Version directly. A WWW-Authenticate: Payment header is also emitted for agents that speak the Merchant Payment Protocol.

HTTP/1.1 402 Payment Required
Content-Type: application/json
WWW-Authenticate: Payment realm="krawl", methods="x402", discovery="https://api.krawl.sh/discovery/resources"
{
  "x402Version": 1,
  "error": "X-PAYMENT header is required",
  "accepts": [
    {
      "scheme": "exact",
      "network": "base",
      "maxAmountRequired": "1000000",
      "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
      "payTo": "0xYourReceivingWalletAddress",
      "resource": "https://api.krawl.sh/research",
      "description": "Krawl quick research (pay-per-use, non-refundable)",
      "mimeType": "text/event-stream",
      "outputSchema": null,
      "maxTimeoutSeconds": 120,
      "extra": {
        "name": "USDC",
        "version": "2"
      }
    }
  ]
}

The accepts array holds one PaymentRequirements object per acceptable payment. maxAmountRequired is the price for the requested tier (here, 1000000 for a quick krawl). extra.name and extra.version carry the USDC EIP-712 domain name and version used to construct the signature.

3. Agent signs a USDC authorization and retries with X-PAYMENT

The agent builds an EIP-3009 transfer authorization that pays at least maxAmountRequired USDC to payTo, signs it, and wraps it in a PaymentPayload. That payload is JSON, base64-encoded, and sent in the X-PAYMENT header. The shape Krawl decodes and validates is:

{
  "scheme": "exact",
  "network": "base",
  "payload": {
    "authorization": {
      "to": "0xYourReceivingWalletAddress",
      "value": "1000000"
    }
  }
}
curl -i -N -X POST https://api.krawl.sh/research \
  -H "Content-Type: application/json" \
  -H "X-PAYMENT: eyJzY2hlbWUiOiJleGFjdCIsIm5ldHdvcmsiOiJiYXNlIiwicGF5bG9hZCI6ey4uLn19" \
  -d '{"query": "What is EIP-4844?", "mode": "quick"}'

Most agents will use an x402 client library (for example, the Coinbase x402 SDK) to construct and sign this header automatically.

4. Server verifies, settles, and streams the cited report

Before trusting the facilitator, Krawl does a defense-in-depth check that the signed authorization matches the challenge: same scheme, same network, the payload.authorization.to address equals payTo, and the authorized value is greater than or equal to maxAmountRequired (overpaying is allowed). A mismatch returns 402; a malformed/undecodable X-PAYMENT header returns 400.

If that passes, Krawl calls the facilitator's /verify endpoint, then (after reserving a concurrency slot) its /settle endpoint to broadcast the transfer. Both calls send { "x402Version": 1, "paymentPayload": ..., "paymentRequirements": ... }.

On success the server streams the cited research report as Server-Sent Events (mimeType: text/event-stream) and attaches an X-PAYMENT-RESPONSE header. That header is a base64-encoded JSON settlement receipt:

{
  "success": true,
  "transaction": "0x…",
  "network": "base",
  "payer": "0x…"
}

If settlement fails, the response carries an additional errorReason field and the request is rejected with 402. If the server is at capacity it returns 503 with an empty accepts array without charging, since the slot is reserved before payment is settled.

Discovering prices without triggering a 402

Agents can fetch the full payable-resource catalog up front:

curl https://api.krawl.sh/discovery/resources
{
  "x402Version": 1,
  "enabled": true,
  "facilitator": "https://x402.org/facilitator",
  "resources": [
    {
      "resource": "https://api.krawl.sh/research",
      "method": "POST",
      "description": "Deep multi-source research report (cited markdown).",
      "x402": {
        "scheme": "exact",
        "network": "base",
        "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
        "maxAmountRequired": "5000000",
        "priceUsd": 5.0,
        "payTo": "0xYourReceivingWalletAddress"
      }
    }
  ],
  "payment_methods": ["x402", "api_key"]
}

Network and asset details

SettingValueEnv var
Networkbase (mainnet); base-sepolia for testnetX402_NETWORK
AssetUSDCX402_ASSET_NAME
Asset contract (Base mainnet)0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913X402_ASSET
Asset (EIP-712 domain) version2X402_ASSET_VERSION
Facilitatorhttps://x402.org/facilitatorX402_FACILITATOR_URL
Max payment timeout120 secondsX402_MAX_TIMEOUT_SECONDS

The facilitator handles both /verify and /settle. The verify/settle timeout is aligned with maxTimeoutSeconds so a Base settlement broadcast has room to confirm under congestion without a premature client timeout.

Server x402 configuration

x402 turns on automatically once a receiving wallet address is set on the Krawl server. This section is operator reference — it documents how the Krawl x402 surface is configured. Hosted Krawl (api.krawl.sh) ships with these defaults; the env vars are listed here so the contract between client and server is explicit.

# Enable x402 by setting your receiving wallet (0x… on Base)
X402_PAY_TO=0xYourReceivingWalletAddress

# Choose the network: "base" (mainnet) or "base-sepolia" (testnet)
X402_NETWORK=base

# Prices in atomic USDC units (6 decimals). Defaults shown.
X402_PRICE_SHALLOW=1000000   # $1.00 per quick krawl
X402_PRICE_DEEP=5000000      # $5.00 per deep krawl

# Optional: protect non-research endpoints when x402 is on
KRAWL_DEV_KEY=your-shared-key

For the full list of environment variables, see Getting Started.

x402 and API keys are alternatives

A request authenticated with a valid API key is not charged via x402. The research endpoints accept either form of access:

  • API key present (X-API-Key header): the request is authorized by the key and billed against the key's plan/quota. x402 does not run.
  • No API key, x402 enabled: the request must carry a valid X-PAYMENT header and pays per request in USDC.

You never pay twice. If you hold a key, just send it. See Authentication for keys, plans, and quotas, and Agents & MCP for connecting autonomous agents to Krawl.

Quickstart — runnable code

Trigger a live 402 challenge (curl)

The fastest way to confirm x402 is alive: call a gated endpoint with no key and no X-PAYMENT. The response body is the challenge.

curl -i -X POST https://api.krawl.sh/research \
  -H "Content-Type: application/json" \
  -d '{"query": "What is EIP-4844?", "mode": "quick"}'

You will see HTTP/1.1 402 Payment Required and a JSON body with accepts[0].maxAmountRequired = "1000000" (one quick krawl, $1.00).

Python — sign + pay with the x402 SDK

# pip install x402 web3 httpx
import os, httpx
from eth_account import Account
from x402.client import X402Client

pk = os.environ["AGENT_PRIVATE_KEY"]              # 0x… Base wallet
account = Account.from_key(pk)
client = X402Client(account=account, http=httpx.Client(timeout=600))

resp = client.post(
    "https://api.krawl.sh/research",
    json={"query": "What is EIP-4844?", "mode": "quick"},
    headers={"Accept": "text/event-stream"},
)
# x402 SDK auto-handled the 402 challenge, signed an EIP-3009 transfer,
# replayed with X-PAYMENT, and the server is now streaming SSE.
for line in resp.iter_lines():
    if line.startswith("data: "):
        print(line)
print("settled:", resp.headers.get("X-PAYMENT-RESPONSE"))

Node — sign + pay with the x402 SDK

// npm i x402 viem
import { createX402Client } from "x402";
import { privateKeyToAccount } from "viem/accounts";

const account = privateKeyToAccount(process.env.AGENT_PRIVATE_KEY as `0x${string}`);
const client = createX402Client({ account });

const resp = await client.fetch("https://api.krawl.sh/research", {
  method: "POST",
  headers: { "Content-Type": "application/json", Accept: "text/event-stream" },
  body: JSON.stringify({ query: "What is EIP-4844?", mode: "quick" }),
});

// X-PAYMENT was attached, server settled, body is the SSE stream
const reader = resp.body!.getReader();
const dec = new TextDecoder();
while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  process.stdout.write(dec.decode(value, { stream: true }));
}
console.error("settled:", resp.headers.get("X-PAYMENT-RESPONSE"));

Testnet — Base Sepolia

Point the same code at Base Sepolia by setting the network on your x402 client (or, in a local dev environment, on the server). The challenge will advertise network: "base-sepolia" and Sepolia USDC, and the facilitator will verify against the testnet:

# Local dev: enable testnet on the server.
X402_NETWORK=base-sepolia
X402_ASSET=0x036CbD53842c5426634e7929541eC2318f3dCF7e   # USDC on Base Sepolia
# Python x402 client — explicit network selection
X402Client(account=account, network="base-sepolia")
// Node x402 client — explicit network selection
createX402Client({ account, network: "base-sepolia" })

Funded Sepolia USDC is available from the Circle USDC faucet.

Verify settlement after the call

Every successful x402 response carries X-PAYMENT-RESPONSE (base64 JSON):

echo "<X-PAYMENT-RESPONSE value>" | base64 -d | jq
{
  "success": true,
  "transaction": "0xabc...",
  "network": "base",
  "payer": "0x..."
}

Use transaction to look the transfer up on BaseScan (https://sepolia.basescan.org for testnet). If the facilitator's success flag is false, an errorReason field explains why and the request was rejected with 402.

On this page