Agent-ready checkout for the long tail.
Prove a D2C merchant can be bought from by an AI buyer — with policy bounds,
a Razorpay test-mode money trail, and a batch readiness score you can defend.
Get started · Workflow · Architecture · GitHub
- Product overview
- Key features
- Product structure
- Getting started
- Usage / workflow
- Technical architecture
- HTTP API
- Configuration & customization
- Roadmap
- Contributing
- License
AI shopping agents are starting to buy on behalf of people. A long-tail D2C merchant does not have a ChatGPT app, an MCP storefront, or a payments integration built for machines. They have a catalog with gaps, a card they cannot let an agent drain, and no proof that a refusal was policy rather than a bug.
Aisle answers one question:
Is my store ready to be bought from by an AI agent, and can I prove it?
It is not a consumer chatbot, a public MCP cart, or a Chat2Checkout clone. It is a merchant-facing readiness product: a seeded (or Shopify-ingested) catalog, a hard policy engine in front of every rupee, an LLM buyer agent that may only spend through tools, and a 40-case eval suite whose score is derived from attempts — not from a vanity total.
The demo merchant is North & Co (clean beauty, ~34 SKUs with intentional holes). All money movement is Razorpay test mode. Capture is never taken from a browser callback alone.
| For the merchant | For the reviewer |
|---|---|
| Am I ready? What do I fix first? | Tool I/O, reason codes, webhook payloads |
| Policy as a brake, not a suggestion | Proof no money moved on a blocked attempt |
| Area | What you get |
|---|---|
| Catalog readiness | Per-SKU completeness (price, stock, image, pincodes, returns, …). Missing fields are a work list, not a percentage in isolation. |
| Policy engine | Deterministic quote() before any Razorpay call: per-order cap, daily cap, confirm-above, COD, max qty, blocked SKUs/categories, stock, pincode. |
| Buyer agent | Tool loop: search_catalog, get_product, quote, place_order, pay, recover. LLM when OPENAI_API_KEY is set; otherwise a deterministic buyer so the suite still runs. |
| Razorpay test checkout | Server-side order create, Checkout.js, HMAC checkout verify, webhook HMAC, Payment Link recovery. Payments persist in SQLite. |
| Eval harness | 40 adversarial cases (buy, over-limit, COD, recall, OOS, incomplete, pincode, qty, fail-then-recover). Live runs are stored; dashboard shows a Fixture until you run the suite. |
| Readiness score | 0.35 × catalog + 0.30 × capture precision + 0.25 × policy precision + 0.10 × recovery. |
| Audit trail | Tool calls with hasMoney flags, policy allow/deny, payment status. Attempt drawer is verdict-first. |
| Shopify ingest | https://store/products.json only (HTTPS, no private hosts). Title/price import; delivery and pincodes stay missing on purpose. |
| Operator UI | Dashboard, catalog, policy (save/load), eval runs, test-mode pay page. Mobile header + nav. |
- Payable eval cases pass when the agent reaches a test-mode order.
actualstays pending until a verified checkout signature orpayment.capturedwebhook. The suite does not fake capture. - Recovery cases inject a failed payment in the harness, then issue a Payment Link. Recovered capture still requires Razorpay.
- Shopify public JSON is not a full catalog. That incompleteness is the point of the score.
- There is no public MCP storefront and no live crawl of arbitrary websites.
See Roadmap: hosted webhook URL, production Turso, demo video, operator polish.
Merchant UI Money + policy Agent + eval
───────────── ────────────── ────────────
/ marketing landing quote() ──────────────┐ LLM / deterministic buyer
/app dashboard policy (SQLite) │ tools → search, quote,
/catalog SKU gaps orders.ts ─────────────┼── Razorpay place_order, pay, recover
/policy save guardrails payments + webhooks │ test API
/runs 40-case history checkout verify ───────┘
/pay test checkout
| Route | Role |
|---|---|
/ |
Marketing landing → console |
/app |
Readiness ring, what to fix first, run the suite |
/catalog |
SKU table, completeness, Shopify URL ingest |
/policy |
Live preview + Save (persisted; used by quote and pay) |
/runs, /runs/[id] |
Eval history, attempt drawer, money axis |
/pay, /pay/[orderId] |
Razorpay Checkout, verify, recover link |
src/
app/ Next.js App Router (pages + route handlers)
api/agent/buy Single interactive agent buy
api/catalog/ingest Shopify products.json
api/eval/ Run suite, list/get runs
api/merchant Catalog + policy snapshot
api/payments/ order, verify, recover, GET :orderId
api/policy GET / PUT
api/quote Policy quote
api/webhooks/razorpay
catalog/ policy/ pay/ runs/
components/ App chrome, attempt drawer, shadcn/ui
lib/
agent.ts LLM + deterministic buyer
agent-tools.ts Tool implementations
policy.ts Deterministic quote engine
orders.ts Shared create / recover (HTTP + agent)
eval.ts 40-case runner
eval-cases.ts Suite spec
db.ts LibSQL schema, seed, CRUD
razorpay.ts HMAC + REST
shopify.ts URL allowlist + product map
mock-data.ts North & Co fixture + UI types
public/brand/ Mark and lockup for docs
- Node.js 20+
- npm
- A Razorpay test-mode key pair (optional for policy/catalog eval; required for real test charges)
- An OpenAI-compatible API key (optional; eval falls back to the deterministic buyer)
git clone https://github.com/rakesh0x/aisle.git
cd aisle
npm install
cp .env.example .env| Variable | Required | Purpose |
|---|---|---|
RAZORPAY_KEY_ID |
For pay / live orders | Test key id (rzp_test_…) |
RAZORPAY_KEY_SECRET |
For pay / verify | Signs checkout order_id|payment_id |
RAZORPAY_WEBHOOK_SECRET |
For webhooks | HMAC of the raw webhook body |
OPENAI_API_KEY |
For LLM agent | If unset, suite uses the deterministic buyer |
OPENAI_BASE_URL |
No | Default https://api.openai.com/v1 |
OPENAI_MODEL |
No | Default gpt-4.1-mini |
TURSO_DATABASE_URL |
No | Default file:./data/aisle.db |
TURSO_AUTH_TOKEN |
Turso only | LibSQL auth |
Do not commit .env. Local SQLite lives under ./data/ (gitignored).
npm run dev # http://localhost:3000
npm test # src/lib/*.test.ts
npm run build # production buildSeeded merchant merch_northco (North & Co) is inserted on first boot if missing. Schema (CREATE TABLE IF NOT EXISTS) always runs, including on existing databases.
Razorpay cannot reach localhost. For payment.captured / payment.failed as source of truth:
- Expose the app (ngrok, Cloudflare Tunnel, or a deployed HTTPS URL).
- Dashboard → Webhooks →
https://<host>/api/webhooks/razorpay. - Events:
payment.captured,payment.failed,payment_link.paid. - Set
RAZORPAY_WEBHOOK_SECRETto the webhook secret.
Without a public URL, /api/payments/verify still confirms Checkout via HMAC + a Razorpay payment fetch.
Open /. Until you run the suite, the score is a fixture. Catalog completeness is the heaviest weight — the dashboard names the missing fields, not just “74”.
Open /policy. Caps, confirm-above, COD, blocked SKUs/categories, max qty. Save policy persists to SQLite. Quotes and Razorpay orders use the saved row.
Click Run suite. Forty cases hit search → product → quote → (if allowed) place_order. With an LLM key the model drives the tools; without one the deterministic buyer still has to pick the expected SKU.
curl -X POST http://localhost:3000/api/eval/run \
-H 'Content-Type: application/json' \
-d '{"mode":"deterministic"}'"mode":"llm" requires OPENAI_API_KEY.
Open /pay (or /pay/<aisleOrderId>). Test instruments:
| Instrument | Result |
|---|---|
success@razorpay |
Capture (after verify / webhook) |
failure@razorpay |
Fail — then Send payment link |
4111 1111 1111 1111 |
Test card (any CVV, any future expiry) |
Send payment link is a no-op until the payment is failed. Status captured is never set from the Checkout handler without /api/payments/verify.
On /catalog, paste https://your-store.myshopify.com. Ingest is /products.json only. Seed North & Co SKUs remain; Shopify rows are shp_<id> and usually incomplete until delivery data exists. The 40-case suite is still North & Co.
curl -X POST http://localhost:3000/api/agent/buy \
-H 'Content-Type: application/json' \
-d '{"query":"Buy a moisturizer under ₹500, deliver to 560034","pincode":"560034"}'Returns quote, order (if allowed), and /pay/<id> when an order exists.
flowchart LR
subgraph UI
Dash["Dashboard / Catalog / Policy / Runs"]
Pay["/pay Checkout.js"]
end
subgraph App["Next.js 16 · Node runtime"]
Agent["Buyer agent"]
Quote["policy.quote"]
Orders["orders.ts"]
Eval["eval suite"]
DB[("LibSQL / SQLite")]
end
subgraph Rzp["Razorpay test"]
ROrders["Orders"]
Checkout["Checkout"]
Links["Payment Links"]
WH["Webhooks"]
end
Dash --> Agent
Dash --> Quote
Agent --> Quote
Agent --> Orders
Eval --> Agent
Quote --> DB
Orders --> DB
Orders --> ROrders
Pay --> Checkout
Pay --> Orders
Checkout --> Pay
WH --> DB
Links --> WH
Stack: Next.js 16, React 19, Tailwind 4, shadcn/Base UI, @libsql/client, Razorpay REST + Checkout.js, optional OpenAI-compatible Chat Completions.
Policy is the brake. place_order and HTTP POST /api/payments/order share createMerchantOrder. If quote.allowed is false, Razorpay is not called.
Daily cap is SUM(amount_paise) for the buyer fingerprint for today, statuses created | captured | recovering | recovered. Clients cannot pass a trusted dailySpentPaise on the public order route; eval injects it only inside the harness for OVER_DAILY_CAP cases.
Payment state machine (never overwrite a capture with a failure):
| Current | Event | Next |
|---|---|---|
created |
payment.captured |
captured |
failed / recovering |
payment.captured |
recovered |
created / recovering |
payment.failed |
failed |
captured / recovered |
payment.failed |
ignored |
LibSQL may return 0n / 1n. Flags use explicit integer checks (asBool), not Boolean(0n).
| Code | Kind | Meaning |
|---|---|---|
OVER_ORDER_LIMIT |
Guard | Above per-order cap |
OVER_DAILY_CAP |
Guard | Above daily agent spend |
CONFIRMATION_REQUIRED |
Guard | Above confirm-above, no human yes |
COD_NOT_ALLOWED |
Guard | COD requested, policy off |
SKU_BLOCKED / CATEGORY_BLOCKED |
Guard | Merchant withhold |
QTY_EXCEEDED |
Guard | Above max qty |
OUT_OF_STOCK |
Catalog | Zero or insufficient stock |
INCOMPLETE_CATALOG |
Catalog | Required fields missing |
PINCODE_UNSERVICEABLE |
Catalog | Address not in service list |
policy_blocked is a success of the brake. catalog_exception is merchant to-do.
All JSON. Node.js runtime.
| Method | Path | Notes |
|---|---|---|
GET |
/api/merchant |
Merchant, policy, SKUs |
GET / PUT |
/api/policy |
Persist guardrails |
POST |
/api/quote |
{ skuId, qty, pincode, cod, confirmationReceived } |
POST |
/api/payments/order |
{ aisleOrderId, skuId, amountPaise, … } — amount must match server quote |
GET |
/api/payments/:orderId |
Aisle or Razorpay order id |
POST |
/api/payments/verify |
Checkout signature fields |
POST |
/api/payments/recover |
Payment Link for failed / recovering |
POST |
/api/webhooks/razorpay |
Raw body + x-razorpay-signature |
POST |
/api/eval/run |
{ mode?: "llm" | "deterministic" } |
GET |
/api/eval/runs |
Newest first |
GET |
/api/eval/runs/:id |
Full attempts |
POST |
/api/agent/buy |
{ query, pincode?, qty?, cod? } |
POST |
/api/catalog/ingest |
{ url } Shopify origin |
PUT /api/policy body:
{
"maxOrderPaise": 500000,
"dailyCapPaise": 800000,
"confirmAbovePaise": 150000,
"allowCod": false,
"requireInStock": true,
"blockedCategories": ["adult"],
"blockedSkuIds": ["sku_blocked_001"],
"maxQty": 3
}Integers are paise. Per-order cap cannot exceed daily cap. The UI uses the same contract.
Edit src/lib/eval-cases.ts. Each case has expected, expectedSkuId, and optional pincode, qty, cod, confirmationReceived, dailySpentPaise, injectFailure. Cases are tested against live quote() in src/lib/eval-cases.test.ts.
Point OPENAI_BASE_URL + OPENAI_API_KEY at any Chat Completions-compatible host. Tool schemas live in src/lib/agent-tools.ts.
Default file SQLite. For Turso, set TURSO_DATABASE_URL and TURSO_AUTH_TOKEN. Seed runs only when merchant merch_northco is absent.
DESIGN.md is the visual contract (tokens, type scale, semantic colour). Do not introduce a second palette.
| Status | Item |
|---|---|
| Done | Durable catalog, policy, payments |
| Done | Quote engine + persisted policy on the money path |
| Done | Checkout verify, webhooks, Payment Link recover |
| Done | Buyer agent + 40-case runner + live run store |
| Done | Shopify /products.json ingest |
| Next | Deploy HTTPS + register Razorpay webhooks so batch captures are webhook-true |
| Next | Turso (or equivalent) for a shared demo DB |
| Next | 5-minute walkthrough: score → policy → fail → recover → audit |
| Later | Operator edits for missing catalog fields (pincode, returns) without leaving Aisle |
| Out of scope | Public MCP cart, WhatsApp/voice storefront, ACP/AP2/x402, claiming ChatGPT discovery |
This repo is the Aisle prototype (Razorpay AI Buildathon, track 01 — AI Growth & Agentic Commerce). Small, reviewable changes beat drive-by refactors.
- Fork / branch from
main. npm install && npm test.- Keep policy and payments deterministic; do not mark
capturedwithout verify or webhook. - Match
DESIGN.mdif you touch UI. - Do not commit
.env,data/*.db, or secrets.
npm run lint
npm test
npm run buildPull requests should say why (guardrail, honesty of score, merchant workflow), not only what files changed.
MIT © 2026 Aisle.
Razorpay, Shopify, and OpenAI are trademarks of their owners. Test mode only — no real merchant settlement in this project.