Local chain

Simple paid API

One existing endpoint, fronted by the gateway and paywalled with x402.

The smallest possible integration: one existing HTTP endpoint, fronted by the gateway and paywalled with x402. No free tier, no MCP — point backend.url at your API and this is a complete integration.

examples/simple-paid-api/config.yamlyaml
version: 1

merchant:
  id: simple-paid-api-example
  name: Simple Paid API Example
  publicBaseUrl: ${GATEWAY_PUBLIC_BASE_URL:-http://localhost:8080}

server:
  port: ${GATEWAY_PORT:-8080}
  host: 0.0.0.0

storage:
  receipts:
    driver: sqlite
    path: ${RECEIPT_STORE_PATH:-./data/receipts.sqlite}

protocols:
  http:
    enabled: true
  mcp:
    enabled: false
    mountPath: /mcp

resources:
  # The one paid endpoint. Swap `backend.url` for your own API and this is a
  # complete integration.
  premium_report:
    name: Premium Report
    description: A single paid endpoint on your existing backend. 0.01 USDC per call.
    input:
      type: object
      properties: {}
      additionalProperties: false
    backend:
      type: http
      method: GET
      url: ${MERCHANT_API_BASE_URL:-http://localhost:3000}/api/report
      timeoutMs: 10000
    pricing:
      type: fixed
      amount: "0.01"
      currency: USDC
    expose: [http]
    payments: [x402]

payments:
  x402:
    enabled: true
    network: ${X402_NETWORK:-eip155:31337}
    rpcUrl: ${X402_RPC_URL:-http://127.0.0.1:8545}
    # Placeholder: replace with the deployed MockUSDC address from
    # `.deploy/local.json` after `npm run chain:deploy` (see README.md)
    asset: ${X402_ASSET:-0x000000000000000000000000000000000000dEaD}
    assetName: ${X402_ASSET_NAME:-MockUSDC}
    assetVersion: ${X402_ASSET_VERSION:-2}
    assetDecimals: ${X402_ASSET_DECIMALS:-6}
    # Merchant-controlled settlement destination. NEVER a gateway-owned wallet.
    # LOCAL DEVELOPMENT ONLY - DO NOT FUND (Anvil's well-known account #1).
    payTo: ${MERCHANT_WALLET:-0x70997970C51812dc3A010C7d01b50e0d17dc79C8}
    maxTimeoutSeconds: 120
    facilitator:
      mode: local
      # LOCAL DEVELOPMENT ONLY - DO NOT FUND (Anvil's well-known account #0)
      signerPrivateKey: ${X402_FACILITATOR_PRIVATE_KEY:-0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80}

What it demonstrates

  • A resource with no input declares an explicit closed schema (properties: {}, additionalProperties: false). Omitting input gives the same closed empty schema; declaring it documents that the resource takes no input.
  • protocols.mcp.enabled: false and expose: [http] — not reachable over MCP at all.
  • The facilitator is Anvil account #0 and payTo is account #1, the same layout the local deploy script uses, so the local chain settles for real.

Run it

From a clone of the repository, after npm install. The chain, the merchant and the gateway each stay in the foreground, so give each one its own terminal.

  1. The local chain
    bash
    npm run chain:start
  2. Mock USDC, in a second terminal
    bash
    npm run chain:deploy

    Deploys MockUSDC, funds the buyer and writes .deploy/local.json.

  3. The “existing backend” this example fronts
    bash
    npm run dev:merchant
  4. The gateway, with this example's config
    bash
    AGENT_COMMERCE_CONFIG=examples/simple-paid-api/config.yaml \
    X402_ASSET=$(node -p "require('./.deploy/local.json').asset") \
      npm run dev:gateway
  5. Verify
    bash
    npm run agent-commerce -- validate --config examples/simple-paid-api/config.yaml
    npm run agent-commerce -- doctor --config examples/simple-paid-api/config.yaml
  6. Call it
    bash
    curl -i http://localhost:8080/api/resources/premium_report/invoke -X POST
    # -> 402 Payment Required, with the payment challenge
The config validates standalone with no environment set: every ${VAR:-default} falls back to the local demo stack's defaults. The default X402_ASSET is a placeholder, which is why the gateway step supplies the deployed MockUSDC address from .deploy/local.json.

A real buyer completes the challenge as in Handle a payment; npm run demo:agent is a full worked example.

Source: examples/simple-paid-api