x402 Documentation

How to call Ontario Protocol's paid endpoints from any agent that speaks HTTP. USDC on Base. No API keys.

New: x402 endpoint readiness quickstart (generate a public report_id before asking agents to pay) · agent payment preflight guide (apply /api/agent/can-pay policy before signing) · embed a readiness badge (link badge → signed report; verify integrity + freshness) · list an x402 service (validate payload, rehearse in sandbox, then submit paid listing) · x402 verification tools comparison (which checks an agent should run before payment) · agent decision playbooks (100 Search Console-informed pre-payment workflows) · Search Console indexing playbook (core sitemap, evidence sitemap, and 404 triage).

Overview

Every paid endpoint on Ontario follows the x402 protocol:

  1. Client makes a normal request, no payment header.
  2. Server replies HTTP 402 with a PAYMENT-REQUIRED header (base64 JSON).
  3. Client signs a payment payload (EIP-3009 transferWithAuthorization for USDC) and resends with PAYMENT-SIGNATURE header.
  4. Server forwards to a facilitator (Coinbase by default) for verify + settle, then returns HTTP 200 + PAYMENT-RESPONSE header.

The flow, in pictures

Client                        Ontario                       Coinbase Facilitator
  |  POST /api/x402/...      |                              |
  |------------------------->|                              |
  |  402 PAYMENT-REQUIRED    |                              |
  |<-------------------------|                              |
  |  build + sign            |                              |
  |  PaymentPayload          |                              |
  |  POST + PAYMENT-SIGNATURE|                              |
  |------------------------->|  POST /verify                |
  |                          |----------------------------->|
  |                          |  isValid: true               |
  |                          |<-----------------------------|
  |                          |  POST /settle                |
  |                          |----------------------------->|
  |                          |  success: true, tx: 0x...    |
  |                          |<-----------------------------|
  |  200 + PAYMENT-RESPONSE  |                              |
  |<-------------------------|                              |

curl

# 1. Probe — get the 402 with payment requirements
curl -i -X POST https://ontarioprotocol.com/api/x402/agent-trust-scan \
  -H "Content-Type: application/json" \
  -d '{"agent_id":"my-agent","target_url":"https://example.com"}'

# 2. Decode the PAYMENT-REQUIRED header (base64 → JSON):
echo "$PAYMENT_REQUIRED_HEADER" | base64 -d | jq

# 3. Sign the payload with your wallet (using x402 client SDK, see Python below)
# 4. Resubmit with PAYMENT-SIGNATURE header:
curl -X POST https://ontarioprotocol.com/api/x402/agent-trust-scan \
  -H "Content-Type: application/json" \
  -H "PAYMENT-SIGNATURE: $SIGNED_PAYLOAD_B64" \
  -d '{"agent_id":"my-agent","target_url":"https://example.com"}'

Python

# pip install x402 web3 eth-account requests
# Never paste real keys into source files. Use an env var and a dedicated low-balance wallet.
from x402.client import X402Client
from eth_account import Account
import os

acct  = Account.from_key(os.environ["X402_PRIVATE_KEY"])
client = X402Client(account=acct, default_network="base")

resp = client.post(
    "https://ontarioprotocol.com/api/x402/agent-trust-scan",
    json={
        "agent_id": "my-agent",
        "target_url": "https://example.com",
    },
    max_payment_atomic=10_000,   # 0.01 USDC, prevents over-pay
)
print(resp.json()["report"]["trust_score"])

TypeScript

// npm install x402 viem
// Never hardcode keys. Use an env var (Node) and a dedicated low-balance wallet.
import { createX402Client } from "x402";
import { privateKeyToAccount } from "viem/accounts";

const privateKey = process.env.X402_PRIVATE_KEY;
if (!privateKey) throw new Error("Missing X402_PRIVATE_KEY");
const account = privateKeyToAccount(privateKey);
const client  = createX402Client({ account, defaultNetwork: "base" });

const res = await client.fetch(
  "https://ontarioprotocol.com/api/x402/agent-trust-scan",
  {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      agent_id:   "my-agent",
      target_url: "https://example.com",
    }),
    maxPaymentAtomic: 10_000n,
  }
);
const data = await res.json();

Endpoints

EndpointMethodPriceWhat you get
/api/x402/agent-trust-scan POST 0.01 USDC Submit an agent's card URL or A2A endpoint, receive a structured trust report (security signals, identity claims, declared capabilities). Returns JSON.
/api/x402/reputation/<agent_id> GET 0.001 USDC Look up an agent's accumulated reputation (sourced from on-chain EAS attestations on Base + recent scan history).
/api/x402/list-agent POST 0.10 USDC Submit an AI agent for inclusion in the Ontario Protocol directory. Pays 0.10 USDC up front to deter spam.
/api/x402/list-service POST 0.50 USDC Register your own x402-paid endpoint so AI agents discover it through Ontario Protocol's `/discover`. 0.50 USDC listing fee.
/discover GET Free Machine-readable catalog of all services.
/.well-known/x402.json GET Free Standardised manifest for agent auto-discovery.
/facilitator/supported GET Free Lists (scheme, network) pairs the proxy supports.
/facilitator/verify POST Free Verifies a signed payment off-chain via Coinbase facilitator.
/facilitator/settle POST Free* Submits payment on-chain. Logs split + take rate to ledger.

* "Free" meaning no fee to call the endpoint. Settlement transactions themselves cost gas + the marketplace's 1.5% take rate when applicable.

Marketplace: list a third-party service

POST /api/x402/list-service is a paid marketplace endpoint (0.50 USDC) used to register a third-party x402-paid API so agents can find it through Ontario’s discovery surfaces. On submission, Ontario runs the free readiness verifier against your endpoint and stores the resulting report.

  • Immediately: your submission appears in /listings as PENDING or READY.
  • Only when ready: it enters /discover (default filters) and /.well-known/x402.json when its stored report grades ready.
  • Trust boundary: readiness signals are not a safety guarantee; agents should still preflight and enforce budgets.

Request body

{
  "name": "My Cool API",
  "description": "What it does",
  "category": "data",
  "endpoint": "https://example.com/api/data",
  "method": "GET",
  "price_atomic": 10000,
  "price_usdc": "0.01",
  "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
  "network": "base",
  "owner_url": "https://example.com",
  "owner_contact": "ops@example.com",
  "tags": ["dataset", "ai-agent", "x402"]
}

Tip: run /verify first and submit only after you reach grade=ready with a fresh report.

Before you pay the listing fee, you can validate your payload shape for free: POST /api/x402/list-service/validate (no payment; does not store a listing).

Sandbox rehearsal (no on-chain rails; does not store a listing): POST /sandbox/api/x402/list-service. It behaves like a paid x402 endpoint (HTTP 402 first, then accept a simulated payment payload), so you can test your x402 client integration without spending money.

Machine-readable schema: /.well-known/x402-list-service.schema.json

curl -sS https://ontarioprotocol.com/api/x402/list-service/validate \\
  -H "Content-Type: application/json" \\
  -d '{"name":"Example Service","description":"Paid endpoint","endpoint":"https://example.com/api/paid","method":"POST","price_usdc":"0.01","network":"base","owner_url":"https://example.com"}'

SDK examples: ontario_list_service.py · ontario-list-service.ts

Response body

The response includes the settlement context under payment and the stored listing under listing (including verification_status, verification, and paid_listing_tx).

Directory: list an agent (optional)

POST /api/x402/list-agent (0.10 USDC) registers an agent card URL for the directory. It uses the same auto-verification behavior (a readiness report is attempted and attached if available).

Validate your agent submission payload for free with POST /api/x402/list-agent/validate.

Machine-readable schema: /.well-known/x402-list-agent.schema.json

curl -sS https://ontarioprotocol.com/api/x402/list-agent/validate \\
  -H "Content-Type: application/json" \\
  -d '{"name":"Example Agent","description":"Agent card","agent_card_url":"https://example.com/agent.json","owner_url":"https://example.com"}'

Errors

StatusMeaning
402Payment required. PAYMENT-REQUIRED header tells you the amount + asset.
402 (after retry)Payment was rejected — see error + stage in the JSON body.
400Could not decode PAYMENT-SIGNATURE header.
410Old SaaS URL. Use /discover instead.

Facilitator

By default Ontario delegates verification + settlement to Coinbase's hosted facilitator (limits and rate policies may apply — confirm against Coinbase docs). With X402_USE_PROXY=1 in the server's .env, requests pass through Ontario's proxy_facilitator, which logs every settlement to a public, append-only SQLite ledger and applies the 1.5% take rate on third-party listings.

See /treasury for the live, public ledger summary or call /api/treasury/ledger directly.

Cookbook: production-tested pay-and-scan script

The following is a production-style reference script for paying an x402 endpoint and then running an Ontario scan to record evidence. Download it, set env vars, and run:

# Install deps
pip install x402 requests pyjwt[crypto] cryptography python-dotenv

# Set env (or add to .env)
export CDP_API_KEY="your-cdp-key-id"
export CDP_API_SECRET="base64-ed25519-secret"
export BUYER_WALLET_SECRET="base64-ec-p256-der-private"
export BUYER_WALLET_ADDRESS="0x…your-wallet"

# Run
python pay_and_scan.py

⬇ Download pay_and_scan.py — The complete reference implementation (~210 lines).

Common pitfalls

⚠ EIP-712 domain name must be "USD Coin"

The Base mainnet USDC contract at 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 uses domain.name = "USD Coin" and version = "2" for EIP-712 typed data. If your signature uses "USDC" or version "1", the Coinbase facilitator will reject with "invalid signature".

⚠ x402 Python SDK nonce is bytes, not hex

prepare_payment_header() may return the nonce field as raw bytes. The CDP facilitator requires a hex string. Cast it before signing: if isinstance(nonce, bytes): nonce = nonce.hex()

⚠ x402Version field required in outer body

When calling the CDP facilitator directly, include "x402Version": 1 in the top-level JSON body. Omitting it produces a 400.