Local chain

Free and premium

A free resource and a paid one on the same backend, over HTTP and MCP.

The most common real shape: a free resource that proves the gateway fronts your existing API, plus a paid one behind x402 — both over HTTP and MCP.

examples/free-and-premium/config.yamlyaml
version: 1

merchant:
  id: free-and-premium-example
  name: Free and Premium 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: true
    mountPath: /mcp

resources:
  # --- free: an unpaid endpoint an agent can call with no proof at all -----
  basic_weather:
    name: Basic Weather
    description: Current basic weather for a city. Free, no payment required.
    input:
      type: object
      properties:
        city:
          type: string
          description: City name, e.g. "Berlin"
      required: [city]
      additionalProperties: false
    backend:
      type: http
      method: GET
      url: ${MERCHANT_API_BASE_URL:-http://localhost:3000}/api/weather/{city}
      timeoutMs: 5000
    pricing:
      type: free
    expose: [http, mcp]

  # --- paid: the same backend's premium endpoint, gated by x402 ------------
  premium_report:
    name: Premium Market Report
    description: Latest premium market analysis. 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, mcp]
    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

  • Two resources on one gateway and one backend: free needs no payments; fixed requires at least one.
  • Both declare closed input schemas; the free one still validates its one field.
  • expose: [http, mcp] on both — one definition drives both adapters.

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/free-and-premium/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/free-and-premium/config.yaml
    npm run agent-commerce -- doctor --config examples/free-and-premium/config.yaml
  6. Call it
    bash
    # free — no payment proof needed
    curl -s http://localhost:8080/api/resources/basic_weather/invoke \
      -X POST -H 'content-type: application/json' -d '{"city":"berlin"}'
    
    # paid — 402 without a proof
    curl -i http://localhost:8080/api/resources/premium_report/invoke -X POST
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.

Source: examples/free-and-premium