Base URL (local): http://localhost:8787
Every /api/* route requires a verified Hexclave user. On the frontend:
// 1. Wrap your app (see Hexclave React/Next SDK)
// <HexclaveProvider app={hexclaveClientApp}> ... </HexclaveProvider>
// 2. On every request, attach the Hexclave auth header:
const authHeader = await hexclaveClientApp.getAuthorizationHeader();
const res = await fetch(`${BASE}/api/requirements`, {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: authHeader },
body: JSON.stringify({ query: "..." }),
});Dev mode: with
DEV_AUTH_BYPASS=1(or no Hexclave keys), the backend runs in dev-bypass — auth is skipped and every call uses a fakedev-user.GET /healthreportsauth: "enforced" | "dev-bypass ...". Build the frontend against this now; setDEV_AUTH_BYPASS=0to enforce real Hexclave auth (the frontend must then send a valid Hexclave access token).
Backend is FastAPI (Python). Run it with:
uvicorn app.main:app --reload --port 8787.
Shared models live in app/models.py — mirror them on the
frontend for type safety. Field names are snake_case.
| # | Endpoint | Purpose |
|---|---|---|
| 1 | POST /api/requirements |
Extract structured requirements + negotiation bounds |
| 2 | POST /api/requirements/:id/search |
Find + rank vendor products (Channel3) |
| 2b | POST /api/requirements/:id/refine |
Merge an NL adjustment into the requirement + re-search |
| 5 | GET /api/products/:productId/reviews |
Review digest — Dean's piece |
| 3+4 | POST /api/negotiate |
Open + run negotiation — Edward's piece |
| — | GET /api/negotiate/:id |
Poll negotiation state / transcript |
| — | POST /api/negotiate/:id/check-price |
Price-target tick (call on a schedule) |
| 6 | POST /api/negotiate/:id/approve |
Approve a settled deal |
| 7 | POST /api/negotiate/:id/purchase |
Get Channel3 buy link + record purchase |
| 8 | GET /api/inventory |
List purchases (inventory) |
| 9 | PATCH /api/purchases/:id/delivery |
Update delivery status |
| $ | GET /api/billing |
Credit balance + owned products (Hexclave) |
| $ | POST /api/billing/checkout |
Stripe checkout URL to buy credits |
| 📒 | GET /api/ledger |
Every payment event the agent executed + feedback |
| 📒 | GET /api/graph |
Payment knowledge graph (nodes + edges) |
Prices are never invented. max_limit/initial_offer are set only when the
user states them (in the query or the body); otherwise both are 0. 0 means
"no ceiling" — /search then applies no price filter and ranks on fit alone.
(Negotiation needs a real max_limit; with 0 it has no guardrail — the
current product flow is search → refine → buy link, no negotiation step.)
// 200 — sorted by fit_score desc
{ "candidates": [ { "product_id": "...", "title": "...", "price": 1099,
"rating": 4.5, "fit_score": 0.92, "rationale": "..." } ] }// body — a natural-language adjustment to the SAME requirement (id = the loop context)
{ "message": "actually under $180 and over-ear only" }
// 200 — requirement updated in place, search re-run with the merged criteria
{ "requirement": { "id": "req_...", "item": "...", "specs": { "style": "over-ear" },
"initial_offer": 160, "max_limit": 180, "currency": "USD" },
"candidates": [ /* same shape as /search, re-ranked */ ] }{ "digest": { "product_id": "...", "rating": 4.5, "review_count": 320,
"pros": [...], "cons": [...], "verdict": "...",
"warranty": "...", "financing_notes": "...", "sources": [...] } }// body
{ "requirement_id": "req_...", "product_id": "...",
"mode": "both", // "simulated" | "price_target" | "both"
"canonical_product_id": "..." } // optional — enables real price tracking
// 200
{ "session": { "id": "neg_...", "status": "awaiting_approval",
"agreed_price": 1049, "rounds": [ {role, offer, message, ts} ] } }
// 402 if out of credits
{ "detail": "insufficient_credits" }status flow: active → awaiting_approval → approved → purchased
(or walked_away if the seller won't meet the ceiling).
POST /api/negotiate/:id/approve // 200 { session: { status: "approved" } }
POST /api/negotiate/:id/purchase // 200 { purchase: {...}, buyUrl: "https://..." }buyUrl is the Channel3 monetized checkout link — send the user there to pay
the merchant.
GET /api/inventory // { items: PurchaseRecord[] }
PATCH /api/purchases/:id/delivery // body { status: "shipped"|"in_transit"|"delivered" }GET /api/billing // { credits, creditCostPerNegotiation, meteringEnabled }
POST /api/billing/checkout // body { product_id?, return_url? } -> { checkoutUrl }Every payment the agent makes (credit charges, checkouts, merchant purchases) is
appended to a durable session log (data/payment-log.jsonl) and projected into a
graph. purchase events carry feedback: agreed_price, list_price,
savings_vs_list, negotiation_rounds.
GET /api/ledger // { events: PaymentEvent[] }
GET /api/graph // { nodes: GraphNode[], edges: GraphEdge[] }
// graph edges: CHARGED, NEGOTIATED, RESULTED_IN, PAID_FOR, SUPPLIED_BY
// e.g. (User) -PAID_FOR-> (Product) -SUPPLIED_BY-> (Merchant)
// (User) -NEGOTIATED-> (NegotiationSession) -RESULTED_IN-> (Product)- Intake form →
POST /api/requirements - Show ranked candidates ←
POST /api/requirements/:id/search - Per candidate, lazy-load reviews ←
GET /api/products/:id/reviews - User picks one →
POST /api/negotiate→ stream/pollGET /api/negotiate/:idand render therounds[]transcript live - On
awaiting_approval, show agreed price → Approve button → Purchase button - Open
buyUrl, then show it in the Inventory view (GET /api/inventory) - Header widget: credit balance from
GET /api/billing; "Buy credits" →POST /api/billing/checkout→ redirect tocheckoutUrl