Merchant setup

Describe your API

Turn existing endpoints into resources — by hand, from an OpenAPI document, or with the interactive wizard.

A resource is one capability an agent can invoke: an endpoint on your backend, plus its input schema, its price and where it is exposed. The map key is the resource id — it is the MCP tool name and the HTTP path segment, so it must be unique and use only A-Z a-z 0-9 . _ -.

Create the resources

config.yamlyaml
resources:
  basic_weather:                        # free: no payments block needed
    name: Basic Weather
    description: Current basic weather for a city.
    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}/api/weather/{city}
      timeoutMs: 5000
    pricing:
      type: free
    expose: [http, mcp]

  premium_report:                       # paid: 0.01 USDC per call
    name: Premium Market Report
    input:
      type: object
      properties: {}
      additionalProperties: false
    backend:
      type: http
      method: GET
      url: ${MERCHANT_API_BASE_URL}/api/report
      timeoutMs: 10000
      headers:
        Authorization: Bearer ${BACKEND_TOKEN}
    pricing:
      type: fixed
      amount: "0.01"
      currency: USDC
    expose: [http, mcp]
    payments: [x402]

{city} is filled from validated input and URL-encoded. Input the URL does not consume becomes the query string on GET/DELETE and the JSON body otherwise.

Input schemas

Schemas are closed by default at every level: an object that omits additionalProperties gets false. A resource with no input accepts nothing. The validator enforces type, properties, required, additionalProperties, primitives and enum; pattern, format, minLength and friends are ignored, and validate warns when you use them.

Path, query and body together

When one operation needs all three, name where each comes from with backend.inputBindings. Input that no binding names is not forwarded at all.

yaml
  create_order:
    name: Create an order
    input:
      type: object
      properties:
        path:
          type: object
          properties:
            userId: { type: string }
          required: [userId]
          additionalProperties: false
        query:
          type: object
          properties:
            notify: { type: boolean }
          additionalProperties: false
        body:
          type: object
          properties:
            productId: { type: string }
            quantity: { type: integer }
          required: [productId]
          additionalProperties: false
      required: [path, body]
      additionalProperties: false
    backend:
      type: http
      method: POST
      url: ${MERCHANT_API_BASE_URL}/users/{userId}/orders
      inputBindings:
        path: path
        query: query
        body: body
    pricing:
      type: free
    expose: [http, mcp]

The binding rules are checked at load and again before pricing, so a malformed request never settles a payment and then fails to reach your backend. See backend.inputBindings.

Pricing

pricing.typeNeedsBehaviour
freenothingdelivered without a payment proof
fixedamount (decimal string), currency, and paymentsanswers 402 until a valid proof is presented
dynamicrejected at load
Backend URLs are administrator configurationThey are never taken from request input, redirects are not followed, and every call is bounded by a timeout and a 1 MB response cap. There is no private-address blocklist: the gateway calls whatever you configure.