gratitude5dee/zap (the agent) and gratitude5dee/airv2 (the rails).
Read Commerce first for the step syntax. This page covers
what a commerce agent is allowed to do, the exact catalog entry it writes, and
what air does with it.
Run the bundled agents
--json is plan-only: it quotes the media spend, reports charges: false, and
writes nothing. --live is required to stage, and it must run inside the
creator’s air box (or with ZAP_AIR_API_BASE and ZAP_AIR_GATEWAY_TOKEN set).
Without either, the step fails closed with COMMERCE_UNCONFIGURED before it
touches the catalog.
What a commerce agent may do
Two step kinds exist, and both only file an owner decision:commerce.stage_listingmerges one entry into~/.hermes/miniapps/shop/catalog.json({ "items": [...] }), thenPOST /api/miniapps/commercewith{"action":"publish_catalog"}. air files a pendingshop_publishdecision, or reuses the one already open (decisionReused: true).commerce.payment_requestposts{"action":"payment_request", ...}. air files a pendingpayment_requestdecision.
storefront_products, or approves a decision. The hosted runner
at zap.wzrd.tech refuses live commerce runs (plan-only runs still work there);
staging belongs to the box that owns the catalog.
The catalog entry
Every listing step writes an entry of this shape. This is the shared fixture both repos test against (tests/commerce-steps.test.ts here,
apps/web/lib/commerce/commerce.test.ts in airv2 under
Zap-staged listings (commerce.stage_listing)):
merch-drop recipe fills the same fields from its own inputs:
description is
Limited merch drop: {PRODUCT_NAME}. Art generated from the creator's own photo.,
inventory is user.INVENTORY (null when left empty), and source.stepId
is the recipe’s step id, stage_listing.
For tickets, kind is event_ticket and inventory is the ticket count, or
null for unlimited.
air’s sanitizeCatalogItem (apps/web/lib/commerce/catalog.ts) decides what
survives approval:
Fields are camelCase. air also accepts the snake_case aliases
price_cents
and image_url. When both spellings are present the camelCase key wins, even
if its value is null, so "imageUrl": null clears an image rather than
falling back to a stale image_url. The Zap CLI always writes camelCase; the
aliases exist for hand-edited catalogs.
The image URL comes from POST /api/media/publish, which the live step calls
before writing the entry. If the upload is refused, the entry is still staged
with imageUrl: null and the run reports why in imageNote. A remote image URL
passed as the input is kept only when it is already under air’s media base
(ZAP_AIR_MEDIA_BASE, default https://media.wzrd.tech); any other host stages
as imageUrl: null up front rather than being silently dropped at approval.
What air does after staging
A staged-but-unapproved listing has no product row, so
startCheckout throws
not found and Stripe is never called. The airv2 test block asserts every row
of this table.
Live run output
A livecommerce.stage_listing step returns:
decisionId. They never report a purchase URL, and never
describe the listing as live; agent/instructions.md spells this out for the
Zap agent.
Improve staged listings (catalog-listings skill)
agent/skills/catalog-listings/SKILL.md is a prompt/logic port of
commerce-agents merchant-agent/skills/catalog-listings: read the staged
record, write the weak or missing copy out in full, stage it, and let the owner
approve. It reasons over the same box catalog.json the Zaps write to, so it
works on whatever merch-drop / event-ticket staged.
Three Eve tools back it, and zap listings exposes the same logic in the box:
Guardrails (ported from
MerchantAgentConfig / check_guardrails, sized to
air’s sanitizeCatalogItem):
- Content fields only:
name(≤ 200),description(≤ 2000),kind(physical | digital | service | event_ticket). priceCentsandinventoryare refused — re-run the Zap with newPRICE_CENTS/INVENTORYinputs.key,imageUrl,active,sourceare protected.- ≤ 25 lines per change, one line per (listing, field) — keys compare
case-insensitively, so
Show-Nightandshow-nightare one listing — a staging note is required, andstage_listing_updaterefuses a target the agent has not read withget_listingthis session. The read is the whole content record: ifname,description, orkindmoved since the read, the stage is refused even when the edit touches a different field. - Entries that air would refuse or strip at approval (bad
key,priceCents,inventory, or animageUrloff the media base) are reported as skipped and cannot be edited; a copy edit must never republish an entry that loses its image. SetZAP_AIR_MEDIA_BASEwhen air runs with a non-defaultR2_PUBLIC_BASE_URL. - Guardrails are re-checked under the catalog lock before the write; a violation stages nothing and calls air nothing.
- The lock spans the
publish_catalogrequest. A refused publish rolls the edits back. A lost reply (timeout) is retried once — air reuses the pendingshop_publishdecision and does not repeat the note — and if the reply is lost again the edits are rolled back and the error (COMMERCE_STAGE_TIMEOUT) says air may already hold the decision; staging again converges on it.payment_requestis never retried, because a repeat would file a second request: a timeout, a dropped connection, or a reply whose body never arrives is reported as possibly filed and is not retryable; check Needs You first.
zap listings update plans by default (prints the diff and the guardrail
result, exit 1 if blocked). --live merges the edit and POSTs
{"action":"publish_catalog"}, which files or refreshes the owner’s
shop_publish decision; the storefront changes only after approval. Off-box,
both the CLI and the Eve tool fail closed with COMMERCE_UNCONFIGURED before
touching anything. Nothing in this flow charges.
Write your own commerce agent
Followskills/zap-authoring/SKILL.md, then end the recipe with a listing
step whose image names an earlier image.* step:
priceCents and inventory accept a literal or a user.<INPUT> reference.
image must name an image.* step that appears above the listing step in
Zap.md, or a declared image input; validate rejects a reference to a later
step. Keep budget.cap_usd small: the only
spend is the art. Say in SKILL.md that the step stages for owner approval
and charges nothing.
Before shipping, run npm run cli -- validate, npm run cli -- lint,
npm test, and npm run typecheck.
Invariants
- No Zap step ever charges or moves money. Every commerce result carries
charges: false. - Publishing is decision-gated. Nothing reaches
storefront_productsuntil the owner approvesshop_publish. - Prices are never client-supplied.
startCheckouttakes a key and a quantity; the amount comes from the projected row. - Only R2-hosted images are projected. Everything else is dropped to
null.
What this does not touch
The box task router (connectivity/taskrouter.py in the zap-light and
zap-heavy-* templates) routes model calls for the box agent; commerce steps
call air’s HTTP rails directly and never pass through it. Weight-level
training and self-improvement loops are out of scope for commerce agents; the
V10 spec in airv2 goal.md gates them behind a separate phase.