Reference

Configuration

The complete config.yaml reference: every block, every rule the loader enforces, and why.

One file, config.yaml, validated before the server starts. Generate one with agent-commerce init or start from config.example.yaml.

Principles

  • Validated, not trusted. YAML is parsed, then validated with Zod. Unknown keys fail rather than being ignored.
  • Fail before startup. An invalid configuration stops the process with a message naming the file, the path and what was expected.
  • Secrets by reference only. ${VAR} placeholders resolve from the environment; an unresolved variable is an error that names the variable.
  • Explicit version. version: 1. A future version is rejected, not guessed.

File location

The loader resolves the config path in this order:

  1. --config

    The explicit path passed to validate or doctor (or loadConfig({ path }) in code).

  2. AGENT_COMMERCE_CONFIG

    The environment variable, resolved against the working directory.

  3. config.yaml

    The default, in the working directory.

Top level

KeyRequiredPurpose
versionyesmust be 1
merchantyesid, name, publicBaseUrl
serveryesport, host, adminToken, allowedOrigins
storage.receiptsyesdriver: sqlite, path
protocolsyeswhich surfaces are enabled
resourcesyesthe capabilities you expose
paymentswhen a paid resource existsrail configuration
authorizationnoAP2 mandate verification
config.yamlyaml
version: 1

merchant:
  id: demo-store
  name: Demo Data Store
  publicBaseUrl: ${GATEWAY_PUBLIC_BASE_URL}

server:
  port: ${GATEWAY_PORT}
  host: 0.0.0.0
  adminToken: ${ADMIN_TOKEN}        # unset => operator routes 404
  allowedOrigins:                   # browsers only; empty by default
    - ${DASHBOARD_ORIGIN}

storage:
  receipts:
    driver: sqlite
    path: ${RECEIPT_STORE_PATH}

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

Resources

The map key is the resource id: it is the MCP tool name and the HTTP path segment, so it must be unique and a legal tool name (A-Z a-z 0-9 . _ -).

yaml
resources:
  market_report:
    name: Premium Market Report
    description: Latest premium market analysis.
    input:                              # JSON Schema for the agent-visible input
      type: object
      properties: {}
      additionalProperties: false
    backend:
      type: http
      method: GET
      url: ${MERCHANT_API_BASE_URL}/api/report
      timeoutMs: 10000                  # bounded, always
      headers:                          # secrets by reference only
        Authorization: Bearer ${BACKEND_TOKEN}
    pricing:
      type: fixed                       # free | fixed (dynamic is rejected)
      amount: "0.01"                    # decimal string, never a float
      currency: USDC
    expose: [http, mcp]
    payments: [x402]

Resource fields

namestringrequired
Human-readable name, shown to agents.
descriptionstring
Shown to agents; for a paid MCP tool the price is appended automatically.
inputJSON Schema
Closed by default at every level. A resource with no input gets an empty closed schema — declaring nothing means accepting nothing.
backend.methodGET | POST | PUT | PATCH | DELETErequired
backend.urlstringrequired
Supports {param} templating from validated input, URL-encoded. Administrator configuration only.
backend.timeoutMsnumber
Explicit timeout on every outbound call.
backend.headersmap
Static headers, typically credentials via ${VAR}.
backend.inputBindingsobject
Which top-level input properties carry the path, query and body. See below.
pricing.typefree | fixedrequired
pricing.amountdecimal string
Required when fixed. Display units, e.g. "0.01".
expose(http | mcp | a2a | acp)[]required
Each protocol named here must also be enabled under protocols.
paymentsstring[]
Required on a paid resource, e.g. [x402].
authorization.requiredstring[]
e.g. [ap2]. Only on paid resources.

backend.inputBindings

Without it (the legacy behaviour), {param} values come from top-level input and everything left over becomes the query string for GET/DELETE or the JSON body otherwise. With it, each group is sourced independently, so one operation can carry path, query and body at once — and input no binding names is not forwarded at all.

yaml
    input:
      type: object
      properties:
        path:
          type: object
          properties:
            userId: { type: string }
          required: [userId]
        query:
          type: object
          properties:
            notify: { type: boolean }
        body:
          type: object
          properties:
            productId: { type: string }
      required: [path]
    backend:
      type: http
      method: POST
      url: ${MERCHANT_API_BASE_URL}/users/{userId}/orders
      inputBindings:
        path: path
        query: query
        body: body

Config load rejects, before the gateway starts:

  • a binding to a property the input schema does not declare;
  • path or query bound to something that is not an object schema;
  • two locations bound to one property, or a binding to the reserved _payment;
  • a body binding on GET or DELETE;
  • explicit bindings with no path binding while url is templated;
  • a path group not in the input's required, or a {param} not declared and required inside it.

Payments

yaml
payments:
  x402:
    enabled: true
    network: ${X402_NETWORK}            # CAIP-2; eip155:84532 locally
    rpcUrl: ${X402_RPC_URL}
    asset: ${X402_ASSET}                # ERC-20 with EIP-3009
    assetName: ${X402_ASSET_NAME}       # EIP-712 domain name
    assetVersion: ${X402_ASSET_VERSION}
    assetDecimals: ${X402_ASSET_DECIMALS}
    payTo: ${MERCHANT_WALLET}           # merchant-controlled. NEVER the gateway's.
    maxTimeoutSeconds: 120
    facilitator:
      mode: local                       # local dev chain only
      signerPrivateKey: ${X402_FACILITATOR_PRIVATE_KEY}

payments.x402

networkCAIP-2required
eip155:84532 (Base Sepolia / local) or eip155:8453 (Base). Anything else is CONFIG_INVALID.
rpcUrlstringrequired
Used for health checks; with a remote facilitator, the chain work is the facilitator's.
assetaddressrequired
ERC-20 with EIP-3009. On mainnet it must be USDC on Base.
assetNamestringrequired
The EIP-712 domain name, not the symbol — USDC on Base Sepolia, USD Coin on Base.
assetVersion / assetDecimalsrequired
payToaddressrequired
Merchant-controlled destination. Never the gateway's. Zero and implausible addresses are refused.
maxTimeoutSecondsnumber
How long a payment challenge stays valid.
facilitatorobjectrequired
mode: local with signerPrivateKey (dev chain only) or mode: remote with url and auth.
allowMainnetboolean
Required to be true on eip155:8453. Never a default.
allowUnauthenticatedFacilitatorboolean
Required on mainnet when facilitator.auth.type is none.

Network specifics and mainnet guardrails: Networks & facilitators.

protocols.acp

yaml
protocols:
  acp:
    enabled: true
    mountPath: /acp            # default
    auth:
      type: bearer             # the only scheme in this release
      token: ${ACP_BEARER_TOKEN}
    idempotency:
      path: ./data/acp-idempotency.sqlite
      retentionHours: 24       # default, and the floor
    checkout:
      operations:              # all five, each on its own resource
        createCheckoutSession: acp_checkout_create
        updateCheckoutSession: acp_checkout_update
        getCheckoutSession: acp_checkout_get
        completeCheckoutSession: acp_checkout_complete
        cancelCheckoutSession: acp_checkout_cancel
    discovery:                 # optional
      documentationUrl: https://merchant.example.com/docs/acp
      supportedCurrencies: [usd]
      supportedLocales: [en-US]

Every mapped resource must exist, carry expose: [acp], and be free with no payments. A full example is in ACP checkout.

authorization.ap2

yaml
authorization:
  ap2:
    enabled: true
    specVersion: "0.2.0"       # the only supported value
    mode: direct               # the only supported mode
    clockSkewSeconds: 60       # default; 300 is the ceiling
    replay:
      path: ./data/ap2-authorizations.sqlite   # its own file
    trust:
      mandateIssuers:
        - issuer: https://surface.example
          audience: merchant.example
          keys:
            - kid: mandate-2026-01
              jwk: { kty: EC, crv: P-256, x: "...", y: "..." }
      checkoutIssuers:
        - issuer: https://merchant.example
          audience: agent-commerce
          keys:
            - kid: checkout-2026-01
              jwk: { kty: EC, crv: P-256, x: "...", y: "..." }

Public keys only; nothing is fetched. See AP2 mandates.

JSON Schema support

The input validator supports type, properties, required, additionalProperties, primitives and enum. minLength, pattern, format and friends are silently ignored, and agent-commerce validate warns when a resource uses one.

You have no configuration-level way to reject an empty string for a {param}minLength: 1 is not enforced. The gateway rejects empty, . and .. path parameters itself, before any payment is taken; validate everything else in your own API.

Validation rules

agent-commerce validate runs exactly the checks the gateway runs at startup and fails on:

  • unknown keys, missing required fields, unsupported version;
  • unresolved ${VAR}, duplicate resource ids, pricing.type: dynamic;
  • a paid resource with no payments, or naming a disabled payment method;
  • expose values outside [http, mcp, a2a, acp], or naming a disabled protocol;
  • overlapping protocol mounts, or a mount claiming a route the gateway already serves;
  • an incomplete or paid ACP checkout mapping, or retention below 24 hours;
  • an invalid or zero payTo/asset, and every mainnet guardrail.

Environment variables

${VAR} works in any string value, and ${VAR:-default} supplies a fallback. The demo uses these:

.env.example.env
GATEWAY_PORT=8080
ADMIN_TOKEN=local-demo-admin-token
DASHBOARD_ORIGIN=http://localhost:5173
GATEWAY_PUBLIC_BASE_URL=http://localhost:8080
MERCHANT_API_BASE_URL=http://localhost:3000
RECEIPT_STORE_PATH=./data/receipts.sqlite
X402_NETWORK=eip155:84532
X402_RPC_URL=http://localhost:8545
X402_ASSET=0x0000000000000000000000000000000000000000
X402_ASSET_NAME=MockUSDC
X402_ASSET_VERSION=2
X402_ASSET_DECIMALS=6
MERCHANT_WALLET=0x70997970C51812dc3A010C7d01b50e0d17dc79C8
X402_FACILITATOR_PRIVATE_KEY=
The zero X402_ASSET is a placeholder that validation rejects on purpose. After npm run chain:deploy, validate and doctor read .deploy/local.json and fill the local X402_* and MERCHANT_WALLET values automatically.