Skip to main content
A commerce agent is a Zap that ends in a storefront listing instead of a video. The Zap generates the art, stages a catalog entry inside the creator’s air box, and files a decision. The owner approves it once in air’s Needs You queue, and the listing is buyable through Stripe Connect. The Zap never publishes and never charges: it only stages. This page is the contract between the two sides, 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_listing merges one entry into ~/.hermes/miniapps/shop/catalog.json ({ "items": [...] }), then POST /api/miniapps/commerce with {"action":"publish_catalog"}. air files a pending shop_publish decision, or reuses the one already open (decisionReused: true).
  • commerce.payment_request posts {"action":"payment_request", ...}. air files a pending payment_request decision.
Everything else is off limits. There is no step that creates a checkout, calls Stripe, edits 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)):
The bundled 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 live commerce.stage_listing step returns:
Agents report the 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).
  • priceCents and inventory are refused — re-run the Zap with new PRICE_CENTS / INVENTORY inputs. key, imageUrl, active, source are protected.
  • ≤ 25 lines per change, one line per (listing, field) — keys compare case-insensitively, so Show-Night and show-night are one listing — a staging note is required, and stage_listing_update refuses a target the agent has not read with get_listing this session. The read is the whole content record: if name, description, or kind moved 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 an imageUrl off the media base) are reported as skipped and cannot be edited; a copy edit must never republish an entry that loses its image. Set ZAP_AIR_MEDIA_BASE when air runs with a non-default R2_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_catalog request. A refused publish rolls the edits back. A lost reply (timeout) is retried once — air reuses the pending shop_publish decision 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_request is 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

Follow skills/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_products until the owner approves shop_publish.
  • Prices are never client-supplied. startCheckout takes 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.