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:
- --config
The explicit path passed to
validateordoctor(orloadConfig({ path })in code). - AGENT_COMMERCE_CONFIG
The environment variable, resolved against the working directory.
- config.yaml
The default, in the working directory.
Top level
| Key | Required | Purpose |
|---|---|---|
version | yes | must be 1 |
merchant | yes | id, name, publicBaseUrl |
server | yes | port, host, adminToken, allowedOrigins |
storage.receipts | yes | driver: sqlite, path |
protocols | yes | which surfaces are enabled |
resources | yes | the capabilities you expose |
payments | when a paid resource exists | rail configuration |
authorization | no | AP2 mandate verification |
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: /acpResources
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 . _ -).
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
namestringrequireddescriptionstringinputJSON Schemainput gets an empty closed schema — declaring nothing means accepting nothing.backend.methodGET | POST | PUT | PATCH | DELETErequiredbackend.urlstringrequired{param} templating from validated input, URL-encoded. Administrator configuration only.backend.timeoutMsnumberbackend.headersmap${VAR}.backend.inputBindingsobjectpricing.typefree | fixedrequiredpricing.amountdecimal stringfixed. Display units, e.g. "0.01".expose(http | mcp | a2a | acp)[]requiredprotocols.paymentsstring[][x402].authorization.requiredstring[][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.
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: bodyConfig load rejects, before the gateway starts:
- a binding to a property the input schema does not declare;
pathorquerybound to something that is not an object schema;- two locations bound to one property, or a binding to the reserved
_payment; - a
bodybinding onGETorDELETE; - explicit bindings with no
pathbinding whileurlis templated; - a path group not in the input's
required, or a{param}not declared and required inside it.
Payments
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-2requiredeip155:84532 (Base Sepolia / local) or eip155:8453 (Base). Anything else is CONFIG_INVALID.rpcUrlstringrequiredassetaddressrequiredassetNamestringrequiredUSDC on Base Sepolia, USD Coin on Base.assetVersion / assetDecimalsrequiredpayToaddressrequiredmaxTimeoutSecondsnumberfacilitatorobjectrequiredmode: local with signerPrivateKey (dev chain only) or mode: remote with url and auth.allowMainnetbooleantrue on eip155:8453. Never a default.allowUnauthenticatedFacilitatorbooleanfacilitator.auth.type is none.Network specifics and mainnet guardrails: Networks & facilitators.
protocols.acp
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
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.
{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; exposevalues 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:
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=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.