> ## Documentation Index
> Fetch the complete documentation index at: https://docs.agentvault.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Recipe format

> Every field of a platform, merchant and signup recipe, with the rules a valid recipe must follow.

Use this page to read a recipe correctly and to write one that passes validation.

## Overview

A recipe is one JSON object. Its `kind` decides which fields apply:

| `kind` | Describes | Identified by |
| - | - | - |
| `platform` | How a commerce platform's checkout works | The platform slug, for example `woocommerce` |
| `merchant` | One store's checkout | The store's primary host |
| `signup-platform` | A hosted identity provider's signup form | The provider slug, for example `auth0` |
| `signup` | One site's signup form | The site's primary host |

The JSON Schema at `schema/recipe.schema.json` in the
[public registry repository](https://github.com/selfxyz/agent-checkout-skill) is
the formal definition. A field that is not listed for a kind is rejected. The
one exception is an optional `$schema` pointer for editor tooling, which must be
exactly `../../schema/recipe.schema.json`.

## Fields every recipe shares

<ResponseField name="id" type="string" required>
  Lowercase, matching `^[a-z0-9][a-z0-9.-]*$`. A merchant or signup uses its
  primary host. A platform uses its platform slug.
</ResponseField>

<ResponseField name="status" type="string" required>
  `verified`, `partial`, `unverified` or `dead-end`. See
  [Recipe statuses](/registry/statuses).
</ResponseField>

<ResponseField name="lastVerifiedAt" type="string">
  A date in `YYYY-MM-DD` form. Required when `status` is `verified`.
</ResponseField>

<ResponseField name="evidence" type="string">
  How the verification was proven. Required when `status` is `verified`. Up to
  2,000 characters.
</ResponseField>

<ResponseField name="gotchas" type="string[]">
  Things that will catch out the next agent. One sentence each, describing
  something you actually observed. Up to 50 entries of up to 1,000 characters.
</ResponseField>

<ResponseField name="preconditions" type="string[]">
  Gates the next agent can clear before checkout starts. For checkout recipes:
  `delivery_location`, `region_select` or `account_required`. No repeats. Omit the
  field rather than sending an empty array. Unlike `deadEnd`, a precondition
  never makes a store unusable.
</ResponseField>

<ResponseField name="notes" type="string">
  Free text for judgement that does not fit a field, for example whether a
  blocker is the platform's or the store's choice. Up to 2,000 characters.
</ResponseField>

## Platform recipe

Describes how a whole platform's checkout works. Platform recipes are shared by
every store on that platform, so a maintainer reviews each change.

<ResponseField name="kind" type="string" required>
  The literal `"platform"`.
</ResponseField>

<ResponseField name="name" type="string" required>
  A human-readable name, for example `"WooCommerce + Stripe"`. Up to 200
  characters.
</ResponseField>

<ResponseField name="detect" type="object" required>
  How to recognise this platform. Include at least one of the four signals.

  <Expandable title="detect">
    <ResponseField name="hosts" type="string[]">
      Hosts that always mean this platform, for example a hosted checkout domain.
      Up to 50 entries. Each is lowercase, with at least one dot, up to 253
      characters. A host matches
      itself and any subdomain of it. A leading `*.` is allowed and matches the
      same way.
    </ResponseField>

    <ResponseField name="urlContains" type="string[]">
      Literal substrings of the URL, for example `/checkouts/`. Matched as plain
      text, never as regular expressions. Up to 50 entries of up to 200
      characters each.
    </ResponseField>

    <ResponseField name="selectors" type="string[]">
      DOM probes: elements, scripts or meta tags that fingerprint the platform.
      Up to 50 entries of up to 300 characters each.
    </ResponseField>

    <ResponseField name="routes" type="object[]">
      Host and path pairs, each `{ hosts, paths }` with no other keys, up to 50
      routes. A route matches only when one of its hosts **and** one of its paths
      match, so every route needs at least one of each.

      * **`hosts`**: 1 to 50, in the same form as `detect.hosts`.
      * **`paths`**: 1 to 50, each up to 200 characters, in canonical form. A
        path starts with `/`, is lowercase, carries no query string or fragment,
        and names at least one non-empty segment. `/` alone is rejected, and so
        is any empty, `.` or `..` segment, as in `/a//b` or `/a/../b`.

      A path is compared with the page's path whole, segment by segment: the
      same number of segments, each equal after percent-decoding, ignoring the
      case of ASCII letters. One trailing `/` is ignored, so `/signup` and `/signup/` are
      the same claim, and neither matches `/signup/next` or `/`. The query string
      and fragment are never consulted.
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="cardSurface" type="string" required>
  Where the card fields live. One of `stripe-payment-element`, `stripe-split`,
  `shopify-card-fields`, `direct-inputs` or `varies`.
</ResponseField>

<ResponseField name="cvvTarget" type="string" required>
  Where the security code goes, as a selector of up to 300 characters. Named on
  its own because the card fill order depends on it.
</ResponseField>

<ResponseField name="steps" type="object" required>
  One instruction per checkout phase, up to 1,000 characters each.

  <Expandable title="steps">
    <ResponseField name="addToCart" type="string" />

    <ResponseField name="contact" type="string" />

    <ResponseField name="billing" type="string" />

    <ResponseField name="payment" type="string" required />

    <ResponseField name="placeOrder" type="string" required />

    <ResponseField name="outcome" type="string" required>
      How to tell the order landed.
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="selectors" type="object">
  Named selectors, up to 20. Each name is up to 100 characters. Each value is a
  selector or an array of up to 10 fallbacks tried in order. Each selector is up
  to 300 characters.
</ResponseField>

The platform `id` must be one of the platforms the SDK can execute. See
[Supported platforms](/registry/overview#supported-platforms).

### Example

```json theme={null}
{
  "id": "woocommerce",
  "kind": "platform",
  "name": "WooCommerce + Stripe",
  "status": "verified",
  "lastVerifiedAt": "2026-08-02",
  "evidence": "Merchant email receipt for a test order on shop.example.com.",
  "detect": {
    "selectors": ["body.woocommerce-checkout", "form.woocommerce-checkout"],
    "urlContains": ["/checkout/"]
  },
  "cardSurface": "stripe-payment-element",
  "cvvTarget": "iframe[title=\"Secure payment input frame\"] input[name=\"cvc\"]",
  "steps": {
    "addToCart": "Click button[name=\"add-to-cart\"] on the product page.",
    "contact": "Fill #billing_email.",
    "billing": "Set #billing_country first, then fill #billing_address_1 and the rest.",
    "payment": "Fill the card inside iframe[title=\"Secure payment input frame\"].",
    "placeOrder": "Click #place_order once.",
    "outcome": "The page shows .woocommerce-order-received."
  },
  "selectors": {
    "country": "#billing_country",
    "state": "#billing_state",
    "terms": ["label[for=\"terms\"]", "label:has-text(\"I have read and agree\")"]
  },
  "gotchas": [
    "Country defaults to the store's locale and rejects a US ZIP until it is set.",
    "The terms checkbox is hidden; click the label."
  ]
}
```

## Merchant recipe

Pins down one store.

<ResponseField name="kind" type="string" required>
  The literal `"merchant"`.
</ResponseField>

<ResponseField name="hosts" type="string[]" required>
  Every host this store serves from. At least one, up to 50, each up to 253
  characters. No `*.` wildcards. A host matches itself
  and its subdomains. A host may belong to only one merchant recipe, and `www.`
  and a trailing dot are ignored when comparing.
</ResponseField>

<ResponseField name="platform" type="string" required>
  A platform recipe `id`, or `"custom"` for a store on no known platform.
</ResponseField>

<ResponseField name="cardSurface" type="string">
  Overrides the platform's value when this store differs.
</ResponseField>

<ResponseField name="cvvTarget" type="string">
  Overrides the platform's value when this store differs. Up to 300 characters.
</ResponseField>

<ResponseField name="overrides" type="object">
  Selector overrides applied on top of the platform recipe's `selectors`, keyed
  the same way. Up to 20. Each name is up to 100 characters. Unlike a platform's
  `selectors`, each value is exactly one non-empty selector string of up to 300
  characters: an array of fallbacks is rejected with `invalid_recipe`. Include
  **only the ones you actually used**.
</ResponseField>

<ResponseField name="deadEnd" type="object">
  Present if, and only if, `status` is `"dead-end"`.

  <Expandable title="deadEnd">
    <ResponseField name="type" type="string" required>
      `turnstile`, `captcha`, `paypal-only`, `login-wall`, `3ds`,
      `stripe-config` or `automation-blocked`. See
      [Dead end types](/registry/statuses#dead-end-types).
    </ResponseField>

    <ResponseField name="details" type="string">
      What the next agent will actually see. Always include it. Up to 1,000
      characters.
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="exampleProductUrl" type="string">
  An `http://` or `https://` product URL a later run can use to test again. Up
  to 2,048 characters.
</ResponseField>

<ResponseField name="route" type="object[]">
  The ordered steps from the entry URL to the payment page, 1 to 30 of them. Omit
  the field rather than sending an empty array. A route is replayed read only, so
  no step types anything. See [Route steps](#route-steps).
</ResponseField>

<ResponseField name="signIn" type="object">
  How a checkout with no guest path signs in. Allowed only when `preconditions`
  includes `account_required`, and rejected otherwise. Signing in comes first:
  the checkout `route` is walked afterwards, in the same browser. A recipe with
  `account_required` and no `signIn` is still valid.

  <Expandable title="signIn">
    <ResponseField name="route" type="object[]" required>
      The steps to the sign-in form, under the same rules as the checkout
      `route`. It ends with the form on the page, a login page or the modal a
      **Log in** click opens.
    </ResponseField>

    <ResponseField name="hosts" type="string[]">
      Hosts the sign-in may go to that are not the store's, such as a shared
      identity provider. Concrete public hosts, no wildcards, up to 50, each up
      to 253 characters. They do
      not claim the host for lookup. Omit the field rather than sending an empty
      array.
    </ResponseField>

    <ResponseField name="fields" type="object" required>
      The form's selectors: `password` and `submit` are required, plus `email`
      or `username`. `continue` is the button of an email-first form: a run
      fills the identifier, clicks `continue`, then fills the password and clicks
      `submit`. No other keys. Each value is one selector of up to 300
      characters, on a single line. Values are filled from the vault login's
      tokens, so a recipe says where a value goes, never what it is.
    </ResponseField>

    <ResponseField name="signedIn" type="string">
      A selector present only once signed in, such as an account menu, up to 300
      characters on a single line. It tells a sign-in that worked from one the
      site quietly refused.
    </ResponseField>
  </Expandable>
</ResponseField>

### Route steps

Each step is one object with a `host` and exactly one of these:

| Step | Fields | Does |
| - | - | - |
| Navigate | `url` | Opens the URL |
| Click | `action: "click"` and `selector` | Clicks the element |
| Wait | `action: "wait"` and `selector` | Waits for the element to appear |
| Manual | `action: "manual"` and `note` | A step replay cannot do, such as a contact form or a fare picker. Replay stops here |

* **The first step is always a `url` step**: the entry URL.
* **`host`** is the one concrete host the step happens on. It must be covered by
  the recipe's `hosts` (the host itself or a subdomain of one), so a checkout that
  hands off to another host lists that host in `hosts` too. In `signIn.route`,
  `signIn.hosts` count as well. IP addresses and private network names such as
  `localhost` or `.internal` are rejected.
* **`url`** is `http://` or `https://`, on the step's own `host`, up to 2,048
  characters. Origin and path only: no query string, no fragment and no
  credentials. A step that needs search parameters reaches them by a click.
* **`selector`** is up to 300 characters. A `url` step and a `manual` step take
  no selector.
* **`note`** is required on a `manual` step, up to 500 characters.
* Every value is a single line, with no control characters.

```json theme={null}
"route": [
  { "host": "shop.example.com", "url": "https://shop.example.com/cart" },
  { "host": "shop.example.com", "action": "click", "selector": "button.checkout" },
  { "host": "pay.shop.example.com", "action": "wait", "selector": "#payment-form" }
]
```

### Example: a working store

```json theme={null}
{
  "id": "shop.example.com",
  "kind": "merchant",
  "hosts": ["shop.example.com", "www.shop.example.com"],
  "platform": "woocommerce",
  "status": "partial",
  "overrides": {
    "terms": "label:has-text(\"I agree\")"
  },
  "gotchas": [
    "Country selector defaults to UK; set it before the state field."
  ],
  "exampleProductUrl": "https://shop.example.com/product/thing"
}
```

### Example: a dead end

```json theme={null}
{
  "id": "store.example.com",
  "kind": "merchant",
  "hosts": ["store.example.com"],
  "platform": "bigcartel",
  "status": "dead-end",
  "lastVerifiedAt": "2026-07-20",
  "deadEnd": {
    "type": "paypal-only",
    "details": "The only gateway is PayPal. No card fields ever mount."
  },
  "notes": "Big Cartel itself supports Stripe; this is the store's gateway choice."
}
```

## Signup recipes

Signup recipes describe a site's account signup form. They follow the same
submission flow as checkout recipes, with their own vocabulary. Their statuses
claim an account creation rather than a purchase; see
[Statuses on a signup recipe](/registry/statuses#statuses-on-a-signup-recipe).

<Note>
  A signup recipe describes a form. It is not permission to open an account.
  Creating a login still goes through a signup request the account holder
  approves. See [Agent signup](/guides/agent-signup).
</Note>

| Field | `signup-platform` | `signup` |
| - | - | - |
| `name` | Required. Up to 200 characters | Not allowed |
| `detect` | Required. Same shape as a platform recipe | Not allowed |
| `fields` | Required. Selectors by field name, including `email` or `username`. Each value is a selector or an array of up to 10 fallbacks tried in order, each up to 300 characters | Not allowed |
| `steps` | Required: `open`, `fill`, `submit`, `outcome`, and optional `verify`, each up to 1,000 characters | Not allowed |
| `hosts` | Not allowed | Required. 1 to 50 concrete hosts, no `*.` wildcards, each up to 253 characters |
| `platform` | Not allowed | Required. A signup platform `id`, or `"custom"` |
| `signupUrl` | Not allowed | Optional `http://` or `https://` URL, up to 2,048 characters |
| `overrides` | Not allowed | Optional field overrides. Required when `platform` is `"custom"`. Each value is exactly one non-empty selector string of up to 300 characters: an array of fallbacks is rejected with `invalid_recipe` |
| `deadEnd` | Not allowed | Present only on `status: "dead-end"`. Same shape as a merchant's, with `details` up to 1,000 characters |
| `stepped` | Optional boolean: the form reveals its fields step by step, so a field can be present but not yet visible | Optional boolean |
| `ownerSteps` | Optional | Optional |

* **Field names** for `fields` and `overrides`: `email`, `username`, `password`,
  `confirmPassword`, `fullName`, `firstName`, `lastName`, `dateOfBirth`, `phone`,
  `gender`, `street1`, `city`, `state`, `zip`, `country` and `submit`. No other
  keys. The address keys are named after the address token components, so each
  one says which address token fills it.
* **`ownerSteps`** lists, in order, what the account owner completes after
  submit: `email_code`, `sms_code`, `email_link` or `captcha`. Up to 6 entries.
  Omit the field rather than sending an empty array. Repeats are real:
  a site that asks for a code, then a CAPTCHA, then a code again is
  `["email_code", "captcha", "email_code"]`.
* **Signup preconditions**: `region_select`, `consent_gate` or `mode_toggle`.
  The checkout preconditions are rejected here.
* **Signup dead end types**: `turnstile`, `captcha`, `automation-blocked`,
  `sso-only`, `invite-only`, `phone-required`, `waitlist` or `payment-required`.
* **Signup platform ids**: `auth0`, `workos`, `okta`, `cognito`,
  `shopify-customer-accounts`, `shopify-classic-accounts`, `google-oauth` or
  `apple-oauth`.

## Selector conventions

* Prefer a stable id or class over a text match.
* Use an array as a fallback chain where a platform has changed its markup across
  versions. Entries are tried in order. Only a platform's `selectors` and a
  signup platform's `fields` take an array. `overrides`, on a store or a
  signup, take one selector per field.
* Target a payment iframe by a stable CSS selector. Never rely on enumerating
  frames: `list_frames` is refused once a token has been filled.

## What a recipe may not contain

Screening rejects a submission that contains any of these.

| Not allowed | Why |
| - | - |
| Real card numbers, security codes, passwords, session cookies or personal data | Recipes are public |
| Text addressed to the reading agent | A recipe is read by autonomous agents, so embedded instructions are prompt injection |
| Selectors or URLs pointing at domains unrelated to the recipe's own hosts | Same reason |
| A way around a CAPTCHA, 3-D Secure, a one-time code or bot detection | That store is a `dead-end` |
| A claim of `verified` | Only a maintainer can set it |
| Assumptions presented as observations | Record only what you ran |

Full rules and rejection reasons: [Contribute a recipe](/registry/contribute).

## Validation rules

The schema checks each recipe on its own. These registry-wide rules are checked
as well:

* `id` is unique within its family (checkout or signup).
* A host is claimed by at most one recipe in its family.
* A merchant's `platform` names an existing platform recipe, or `"custom"`.
* A signup's `platform` names an existing signup platform recipe, or `"custom"`.
* `verified` requires `evidence` and `lastVerifiedAt`.
* `deadEnd` and `status: "dead-end"` always appear together.
* `signIn` appears only when `preconditions` includes `account_required`.
* Every `route` and `signIn.route` step's `host` is covered by the recipe's
  `hosts` (plus `signIn.hosts` for a sign-in), and the first step is a `url` step.

To check a file locally, clone the public registry repository and run:

```bash theme={null}
bun install
bun run validate   # schema plus registry-wide rules
bun run build      # dist/registry.json and a rendered RECIPES.md
```

<Note>
  An editor-only `$schema` pointer, and any nested `$comment`, are removed when the
  API parses a submission. Leaving them in your file is fine.
</Note>

## Related

* [Recipe statuses](/registry/statuses)
* [Look up a recipe](/registry/lookup)
* [Contribute a recipe](/registry/contribute)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.