Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ Razorpay's MCP server lets an agent *pay*. Bazaar's MCP lets an agent *find whom

Everyone assumes Shopify-scale catalogs, GTINs and cards. Bazaar compiles a WhatsApp seller's Google Sheet and answers "deliver to 560034 by Friday?" — the 80% of the market the others cannot reach.

**Economics (from the results):** quote maths, ranking and the policy gate never call a model, so cost does not scale with order value. The negotiator — the only model call on the money path — routes to a small model and caches, landing under ₹1 of model cost per completed order against ₹9–14 of take-rate revenue on a typical basket. Revenue lines: a take-rate on agent-originated GMV, a **Razorpay Agentic Plan** for brands with no Razorpay-hosted checkout (Shopify's Agentic plan, for India), and an agent-order protection bundle (RTO Shield + chargeback) priced per agent order.
**Economics (measured, not claimed).** Quote maths, ranking and the policy gate never call a model, so cost does not scale with order value — only the seller's *propose* step does. A cold-cache probe (every call live and metered by the token counters in the LLM client) put the model cost at **₹3.37 per completed order on gpt-4o** — 40 tasks, 27 orders, ₹90.87 total ([`results/gpt4o_costprobe/RESULTS.md`](results/gpt4o_costprobe/RESULTS.md)). That is against ₹9–14 of take-rate revenue on a typical basket, and it drops roughly 10–15× when propose is routed to gpt-4o-mini (or to zero on the free gpt-oss backend), because the deterministic paths carry the rest. Revenue lines: a take-rate on agent-originated GMV, a **Razorpay Agentic Plan** for brands with no Razorpay-hosted checkout (Shopify's Agentic plan, for India), and an agent-order protection bundle (RTO Shield + chargeback) priced per agent order.

## What breaks, and what happens

Expand Down Expand Up @@ -218,6 +218,8 @@ Real incidents from this build, kept because the fixes became the architecture:
| latency p50 / p95 | 47 / 62 ms (deterministic) | a cache hit ≈ the offline figures; a real gpt-4o proposal adds ~1.5–4 s (the p95 tracks how many calls in a run are live vs cached) |
| model failovers during the run | — | 0 |

> **Why report a 0.094?** The held-out `name` score is the *offline dictionary* backend's, and a dictionary literally cannot know a brand it was never given ("Aashirvaad", "Daawat") — that is exactly why a model backend exists, and gpt-4o lifts the same column to 0.469. It is reported anyway because it is honest and because it is **not a safety number**: a name the compiler is unsure of is sent to the merchant's review queue, never guessed, so a low name score costs review clicks, not a wrong price or a wrong order (those fields score 1.000 on both backends). Cost per completed order is a measured number, not a claim — see the economics section above.

### Real agents, not scripts

The 200-task table above is produced by a deterministic scripted buyer — reproducible, and the honest baseline. But "an agent *could*" is weaker than "an agent *did*", so three separate pieces of evidence show real models on the wire:
Expand Down
5 changes: 4 additions & 1 deletion bazaar/compiler/heldout.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ def run_heldout(llm: LLM, heldout_dir: Path, merchants_by_vertical: dict[str, An
rows_total = 0
for csv_path in sorted(heldout_dir.glob("*.csv")):
truth = json.loads(csv_path.with_suffix("").with_suffix(".truth.json").read_text(encoding="utf-8"))
template = merchants_by_vertical[truth["vertical"]].model_copy(update={"products": []})
base = merchants_by_vertical.get(truth["vertical"]) or next(iter(merchants_by_vertical.values()), None)
if base is None:
continue # no merchant loaded to use as a template (e.g. a trimmed corpus)
template = base.model_copy(update={"products": []})
compiled = compile_rows(read_csv(csv_path), template, llm, workers=workers)
rows = truth["rows"]
rows_total += len(rows)
Expand Down
5 changes: 5 additions & 0 deletions bazaar/llm/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,8 @@ def stats(self) -> dict[str, int]:
with self._lock:
n = self._db.execute("SELECT COUNT(*) FROM calls").fetchone()[0]
return {"hits": self.hits, "misses": self.misses, "stored": n}

def usage(self) -> dict[str, float]:
# token/cost accounting lives on the wrapped backend; cache hits never reach it, so
# this reports the cost of the real API calls this run (the misses)
return self.inner.usage() if hasattr(self.inner, "usage") else {}
31 changes: 30 additions & 1 deletion bazaar/llm/openai_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@

from bazaar.llm.base import LLM, LLMError

# USD per 1M tokens (input, output). Only the models we actually route to; extend as needed.
_PRICE_PER_MTOK: dict[str, tuple[float, float]] = {
"gpt-4o": (2.50, 10.00),
"gpt-4o-mini": (0.15, 0.60),
"openai/gpt-oss-120b": (0.0, 0.0), # Groq free tier
"qwen/qwen3.8-27b": (0.0, 0.0),
}
USD_TO_INR = 88.0


class OpenAILLM(LLM):
name = "openai"
Expand All @@ -22,6 +31,24 @@ def __init__(self, api_key: str, model: str = "gpt-4o", base_url: str = "", task
self._client = OpenAI(api_key=api_key, base_url=base_url or None)
self._model = model
self._task_models = dict(task_models or {}) # e.g. {"normalize_product": "gpt-4o-mini"}
# token/cost accounting — only real API calls reach here (cache hits never do), so this
# is a true measurement of what the run spent, not an estimate
self.prompt_tokens = 0
self.completion_tokens = 0
self.usd = 0.0

def usage(self) -> dict[str, float]:
return {"prompt_tokens": self.prompt_tokens, "completion_tokens": self.completion_tokens, "usd": round(self.usd, 4), "inr": round(self.usd * USD_TO_INR, 2)}

def _account(self, model: str, resp) -> None:
u = getattr(resp, "usage", None)
if u is None:
return
pt, ct = int(getattr(u, "prompt_tokens", 0) or 0), int(getattr(u, "completion_tokens", 0) or 0)
self.prompt_tokens += pt
self.completion_tokens += ct
pin, pout = _PRICE_PER_MTOK.get(model, (0.0, 0.0))
self.usd += pt / 1e6 * pin + ct / 1e6 * pout

def complete_json(self, task: str, system: str, user: str, schema: dict[str, Any]) -> dict[str, Any]:
return self._call(task, system, user, schema)
Expand All @@ -32,17 +59,19 @@ def complete_json_image(self, task: str, system: str, user: str, image_b64: str,

def _call(self, task: str, system: str, user: Any, schema: dict[str, Any]) -> dict[str, Any]:
fn_name = f"answer_{task}"
model = self._task_models.get(task, self._model)
tool = {"type": "function", "function": {"name": fn_name, "description": f"Return the structured answer for task '{task}'.", "parameters": schema}}
try:
resp = self._client.chat.completions.create(
model=self._task_models.get(task, self._model),
model=model,
temperature=0,
messages=[{"role": "system", "content": system}, {"role": "user", "content": user}],
tools=[tool],
tool_choice={"type": "function", "function": {"name": fn_name}},
)
except Exception as e: # noqa: BLE001
raise LLMError(str(e)) from e
self._account(model, resp)
choice = resp.choices[0] if resp.choices else None
calls = getattr(choice.message, "tool_calls", None) if choice else None
if not calls:
Expand Down
17 changes: 17 additions & 0 deletions bazaar/simulator/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,13 @@ def log(msg: str) -> None:

if hasattr(llm, "stats"):
report["backend"]["llm_cache"] = llm.stats()
if hasattr(llm, "usage"):
# cost per completed order, measured from real token usage (cache hits cost nothing).
# On a warm-cache replay this is the marginal cost of the calls that ran live this time.
u = llm.usage()
orders = report["transactions"]["orders"]
u["inr_per_order"] = round(u["inr"] / orders, 4) if orders else 0.0
report["backend"]["llm_usage"] = u
if hasattr(llm, "status"):
# provenance: total_failovers=0 proves the real model answered (not the deterministic
# fallback); on a cache replay misses=0 is expected and does not mean the model was skipped
Expand Down Expand Up @@ -335,6 +342,16 @@ def render_markdown(r: dict[str, Any]) -> str:
misses = cache.get("misses", 0)
prov += f" LLM cache: {cache.get('hits', 0)} hits / {misses} misses"
prov += (" — every call served from a prior run's cache; a warm-cache replay costs nothing and re-bills nothing, and the failover count above proves the model, not the fallback, produced the cached answers." if misses == 0 else f" ({misses} real model calls this run; the rest replayed from cache).")
usage = b.get("llm_usage")
if usage and usage.get("prompt_tokens"):
per = usage["inr_per_order"]
prov += (
f"\n\n**Cost.** The live calls this run used {usage['prompt_tokens'] + usage['completion_tokens']:,} tokens "
f"(₹{usage['inr']:.2f} at listed {b.get('model', 'gpt-4o')} rates), which is **₹{per:.2f} per completed order** — "
f"quote maths, ranking and the policy gate never call a model, so only the seller's propose step costs anything, "
f"and it drops ~10–15× on gpt-4o-mini (or to zero on the free gpt-oss backend). "
f"(A warm-cache re-generation bills only the calls that changed, so its per-order figure is lower than this cold-cache measurement.)"
)
lines += ["", prov]
lines += ["", f"_Elapsed {r['elapsed_s']} s._", ""]
return "\n".join(lines)
Expand Down
63 changes: 63 additions & 0 deletions results/gpt4o_costprobe/RESULTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Bazaar — measured results

Generated 2026-09-05T11:57:45Z by `python -m bazaar.simulator.run` (v0.1.0, llm=`openai`, payments=`fake`). Nothing here is hand-edited.

## Catalog compiler (20 merchants, messy CSV → agent-readable catalog)

| field | accuracy |
|---|---|
| name | 0.811 |
| price | 1.000 |
| unit | 0.966 |
| pack_size | 0.948 |
| category | 0.917 |
| gst | 0.925 |
| stock | 0.891 |

Review rate 0.230 (items queued for the merchant instead of guessed) · injections neutralised **10/10** · readiness mean 96.3 (min 91).

**Held-out eval** — 3 hand-written catalogs the generator did not produce (kirana rate card, Shopify export, electronics price list; 32 rows): name 0.500 · price 1.000 · unit 0.969 · pack_size 0.938 · stock 1.000 · gst 1.000 · review rate 0.719. Cells the source doesn't state (e.g. GST on a Shopify export) are review-queued, never guessed.

## Transactions (40 buyer tasks, 25 possible / 15 impossible by construction)

| metric | Bazaar | ablation: same catalog & index, negotiation off, same-city filter, no serviceability answers |
|---|---|---|
| orders | **27** | 23 |
| task → order | 67.5% | 57.5% |
| completion on possible tasks | 100.0% | 84.0% |
| GMV | **₹62,802** | ₹54,230 |
| discounts given (all rule-bounded) | ₹802 | — |
| negotiation rounds | 9 | 0 |

Lift: **+4 orders, +₹8,572 GMV (1.16×)**.

**4 orders (₹8,895) could not have happened at all without Bazaar** — 3 needed a bounded offer, 1 needed a other; 50% of them arrived in Hindi or Hinglish. The net lift is small because bounded discounts also trade margin for completions; this number is the demand that simply does not exist for a merchant without an agent-readable storefront.

Declines on impossible tasks — precision 1.000, recall 1.000; wrong orders on impossible tasks: **0**; wrong declines on possible tasks: 0. Overall task accuracy 100.0%. Errors: 0.

By language: hi-Latn 100.0%, en 100.0%, hi 100.0%. Latency p50 1689.5 ms · p95 4103.8 ms (in-process, llm=`openai`).

## Trust

- Audit entries 258, hash chain intact: **True**, Merkle root `b843402f86485d8e…`
- Explanations present on 100.0% of agent turns
- Grants issued 27, used 27; fairness-ledger entries 9, inconsistencies **0**

## False-positive cost — policy strictness sweep

Same tasks, tighter merchant per-order cap. Wrong declines are *possible* tasks the gate refused; lost GMV is the main-run value of every order the tighter cap prevented (reroutes included). The first row is the default cap and must match the table above.

| per-order cap | orders | wrong declines on possible tasks | lost GMV | wrong orders on impossible tasks |
|---|---|---|---|---|
| ₹50,000 (default) | 27 | **0** | ₹0 | 0 |
| ₹10,000 (Reserve Pay block) | 25 | **2** | ₹36,120 | 0 |
| ₹5,000 | 24 | **3** | ₹42,524 | 0 |
| ₹2,000 | 23 | **4** | ₹46,540 | 0 |

## Provenance

Backend `openai` (model `gpt-4o`). Model failovers to the deterministic fallback during this run: **0** — so the model itself produced these results (health: degraded=False, failures=0).

**Cost.** The live calls this run used 561,032 tokens (₹90.87 at listed gpt-4o rates), which is **₹3.37 per completed order** — quote maths, ranking and the policy gate never call a model, so only the seller's propose step costs anything, and it drops ~10–15× on gpt-4o-mini (or to zero on the free gpt-oss backend). (A warm-cache re-generation bills only the calls that changed, so its per-order figure is lower than this cold-cache measurement.)

_Elapsed 573.3 s._
Loading
Loading