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.
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:
freeneeds nopayments;fixedrequires 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.
- The local chain
bash npm run chain:start - Mock USDC, in a second terminal
bash npm run chain:deployDeploys MockUSDC, funds the buyer and writes
.deploy/local.json. - The “existing backend” this example fronts
bash npm run dev:merchant - 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 - 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 - 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