Writing a recipe
A recipe turns a booking job into a firm quote, a fare menu, or a paid ticket on one supplier's website. Here is the path from zero to an activated domain.
1 — Start from the contract
Clone recipe-spec.
It contains the normative README, the SDK (sdk/index.mjs — zero
dependencies), a complete toy recipe (example.com/recipe.mjs) that
speaks every signal, the JSON Schemas, and the offline conformance suite:
git clone https://github.com/brij-digital/recipe-spec
cd recipe-spec && node test-sdk.mjs # the corpus your recipe must satisfy
2 — The shape of a recipe
One ES module, driven entirely by environment variables, talking to the
runtime only through stdout signals. Three tasks, selected by
TASK:
| Task | Input (env) | Output signal |
|---|---|---|
search | DCITY, ACITY, DDATE (+RDATE round trip) | emitResult("search", {route, count, offers}) |
offer-details | FLIGHT (the offer id) | emitResult("offer-details", {base, fares}) — each fare: price + the supplier's own condition wording |
book | FLIGHT, FARE_PRICE, PRICE_CAP, PAX_LIST, CONTACT_EMAIL, PURCHASE_MODE | emitResult("book", {payClicked, paymentStatus, reference…}) |
The SDK stamps every signal with {v: 1, task} and refuses to
emit a malformed one (exit 7). The runtime revalidates everything
server-side — honesty is structural, not optional.
3 — The rules that matter
- Never buy what wasn't asked — refuse a checkout carrying add-ons the order didn't request. Refusing is free.
- Never book an unverified price — the live total must fit
under
PRICE_CAP, checked where it is exact (the checkout), not on rounded list prices. - payClicked is sacred: emit
payClicked:trueonly if the Pay control was actually activated; emitpaymentStatus:"paid"only on a structural success signal. This is what separates a clean refund from a frozen order. - Exit codes are your error taxonomy (1 bad input · 2 captcha · 3 offer gone · 4 checkout · 5 return leg · 6 passenger rejected · 7 malformed): they feed your public stats, so bail precisely.
- No subprocesses, no runtime-built code, no reaching outside your declared hosts — the static gate rejects them (it rejected our own reference recipe's first submission).
4 — The manifest
protocol_version: 1
domain: yoursupplier.com
recipe: recipe.mjs
flow: guest # guest · ephemeral-account · account
kind: browser # or api (a connector: same signals, no browser)
oracle:
type: email
sender_domains: [yoursupplier.com] # DKIM d= that proves a ticket
coverage:
regions: [EU]
cabins: [Y]
trip_types: [ow, rt]
payment:
currency: USD
max_amount: 150
routing:
partner: your-name
5 — Test locally
node test-sdk.mjs # contract conformance, offline
TASK=search DCITY=mad ACITY=bcn DDATE=20261215 node yourdomain/recipe.mjs
TASK=book … PURCHASE_MODE=dry node yourdomain/recipe.mjs # dry: stops before Pay, no card env
6 — Submit (1 USDC)
POST /recipes/submissions — x402-paid. The paying wallet is
your durable identity: payout, stake and reputation hang off it.
curl -X POST https://travel.brij.fi/recipes/submissions \
-d '{"author_wallet":"…","domain":"yoursupplier.com",
"manifest_yaml":"…","recipe_js":"…"}'
# → {submission_id, status, findings}; poll GET /recipes/submissions/{id}
The pipeline: static review → sandboxed dry-run against the live supplier (runner-collected evidence, synthetic passenger, a dry run that clicks Pay is refused unconditionally) → an AI judge comparing evidence, manifest and code. The response is 200 whether queued or rejected — the findings are what the fee buys. A pass goes to human activation (registry merge + pin), then probation under capped amounts.
7 — Live: watch yourself
GET /recipes shows your domain's settlement funnel
and run telemetry publicly; GET /partner/runs gives you your own
failures with diagnostics and replay ids. Infra failures are attributed to the
platform, never to your recipe. The best recipe holds the domain; a displaced
author keeps a residual finder's fee.
© 2026 Brij Digital · Terms · Privacy · [email protected]