Loop : the self-defending merchant platform
It sells on the way in, and defends on the way out.
What inspired it
Every AI commerce demo I saw stopped at the same place: the payment succeeded, confetti, done. Meanwhile the other half of the job refunds, disputes, chargebacks lived in a completely different tool, disconnected from the conversation that produced the sale.
That gap is where merchants lose money. A chargeback is decided on evidence: what was shown, what the buyer agreed to, what was paid, what shipped. That evidence exists at the moment of purchase and then evaporates into screenshots and memory. By the time a dispute arrives, the merchant is arguing from a place they can no longer prove.
PayPal's 2026 hackathon was the excuse to close that loop. If an agent can sell, it should be able to defend the sale with the same proof it just created. That's the whole project: buy with proof, defend with the same proof.
What I learned
The model should not be allowed to move money. The most useful design decision was splitting "decide" from "act". The agent assesses a claim and proposes a verdict; a deterministic policy gate in plain TypeScript hard $250 cap, 85% confidence floor, a fixed verdict matrix decides whether anything happens. The model can be persuasive or wrong, but it cannot authorize a refund. A cap an environment variable can raise is not a cap.
Buyer messages are data, never instructions. I wrote three adversarial fixtures where the buyer text tries to close delimiters, claim developer authority, or inject a fake evidence preamble. The defense holds because the buyer's words never reach a tool call. Prompt injection stops being a filter problem and becomes a boundary problem.
Evidence captured at purchase is worth more than any argument made later. A SHA-256 hash chain of listing, consent, capture, shipment, and delivery means "the record was altered" is a check you can re-run, not a claim you have to trust.
Deployment is part of the product. Splitting the storefront and console into separate Render deployments was easy; making them agree on URLs, origins, and build-time configuration was the part that actually broke, twice.
How I built it
Loop is a strict-TypeScript pnpm monorepo with three apps and five shared packages:
- Storefront (React 19 + Vite): a chat assistant that calls real commerce tools (
search_products,cart_add,checkout_start), a catalog, a cart, and Fastlane checkout. It also exposes 8 experimental WebMCP tools throughdocument.modelContextwhen the browser supports them, with the built-in assistant as the guaranteed fallback. - API (Fastify + Zod): catalog, carts, checkout, the evidence ledger, the carrier simulator, the PayPal webhook handler, and the defense pipeline. Every response is validated against a Zod schema, including in tests.
- Console (React 19 + Vite): the merchant surface — 12 benchmark scenarios, the policy gate's decision and rationale, side-by-side buyer claim vs. verified ground truth, KPI cards, and a tamper simulation that breaks the chain on demand.
- Packages:
@loop/ledger(canonical JSON, hash chain, verification),@loop/policy(pure, 100% branch coverage),@loop/paypal(typed Orders v2, Fastlane tokens, Disputes API),@loop/domain(agent pipeline, assessor, executor, carrier simulator),@loop/shared(contracts and fixtures).
The buy path creates a real PayPal Sandbox order, the PayPal JS SDK opens the approval popup, and the server captures it with the approved order ID. In parallel the API writes the ledger: ORDER_CREATED, LISTING_SHOWN, CONSENT_GIVEN, PAYPAL_CAPTURED, SHIPPED, DELIVERED.
The defend path treats the claim as untrusted input, verifies the ledger chain first, then the carrier record, then runs the policy gate. CONTEST compiles an evidence packet, CONCEDE accepts a legitimate loss, ESCALATE creates a human task.
Verification is part of the build: 166 Vitest tests across the workspace, a 12-fixture evaluation harness with a confusion matrix, and full branch coverage gating on the policy package.
Deployment is two static sites (storefront, console) plus one shared API on Render, wired with explicit origins and PayPal Sandbox keys. The API holds no secrets in code and runs with real sandbox credentials.
Challenges I ran into
A policy gate that is total. Every combination of amount, confidence, delivery status, address match, dispute state, and ledger validity has to produce a safe answer. Writing the matrix first and the tests to hold it at 100% branch coverage forced clarity I would not have found by coding the happy path.
Deterministic ledgers. Hash chains are unforgiving: key ordering, number formatting, and timestamps all change the hash. Canonical JSON plus a single append path was the only way to make verification reproducible.
The PayPal round trip. Sandbox checkout has three moving parts — server-side order creation, buyer approval in a popup, server-side capture — and each fails differently. Placeholder payment tokens from an earlier mock flow had to be rejected loudly in sandbox mode instead of quietly producing a fake success.
Wiring two real deployments. The frontends bake the API origin at build time; the API pins allowed origins; Render appends a suffix when an onrender.com subdomain is already taken. The first deployment looked fine and showed an empty catalog because the API's CORS allowlist still listed the un-suffixed name. The fix was one env var, but finding it meant reading the deployed bundle and testing the API with the exact browser Origin header.
Keeping the demo honest. The dispute executor records decisions rather than submitting them, because the fixture dispute IDs are synthetic. I would rather say that plainly than let a demo imply a PayPal API call that never happened.
What it doesn't do yet
The dispute assessor is a deterministic stand-in for the LLM slot (the boundary is built; the model is optional). Dispute actions are recorded, not submitted. Checkout identifies shoppers by email only, a deliberate simplification of Fastlane's email + OTP step. State is in memory, so the API is deliberately single-instance.
Built With
- agents
- fastlane
- javascript
- paypal
- typescript