← all work
● ship · 2025 · Creator · Solo build

D2C AI Employee

An AI employee for D2C brands that answers 'am I making money?' with cited numbers, then acts.
D2C AI Employee cover
chat p95
~5s on real Gemini
tests
246 (incl. eval harness)
citation
enforced in architecture
Python · FastAPI PostgreSQL · raw + canonical Redis token bucket Gemini · planner / joiner / composer LLMCompiler-style agent pipeline semantic layer · citation contract vector + BM25 retrieval propose-only agents Next.js · SSE

An AI employee for Indian D2C brands. It answers the question a founder actually loses sleep over, 'net of RTO and ad spend, am I making money on this campaign, SKU, or pincode?', with numbers cited back to source rows, then proposes rupee-saving actions on its own.

The question every founder gets wrong #

Every Indian D2C founder spends half an hour each Monday on one question and still gets it wrong: net of returns and ad spend, is this campaign actually making money? The leak that eats the answer is RTO, return-to-origin on cash-on-delivery orders. RTO runs 28 to 35% on COD, COD is 60 to 70% of order volume, and almost no dashboard nets it out against Meta spend and freight. So founders keep optimising on a gross ROAS that quietly turns negative once returns land.

Net of RTO and ad spend, am I actually making money on this campaign, this SKU, this pincode?

I set out to make that one number trustworthy, and then to have the system act on it without a human babysitting every step. Two halves: a read side that answers with cited numbers, and a write side of agents that propose rupee-saving actions.

Why these three connectors #

The answer lives across three systems, so the first decision was which to pull and how to keep them swappable.

  • Shopify for orders, COGS, and the COD-versus-prepaid split.
  • Meta for campaign and ad spend with attributed conversions.
  • Shiprocket for AWB, courier, RTO status, and freight, which is the leak nobody else can see.

Razorpay was the runner-up and lost: Shopify already records gateway and amount, while without shipment data you simply cannot answer the founder's real question. All three sit behind one connector protocol with a check, streams, and read shape, each testable in isolation against mocked HTTP, and each flips from a local mock to the real API purely on which env vars are set.

From raw to canonical, with provenance #

Numbers are only trustworthy if you can trace them, so the warehouse is two layers: an append-only raw JSONB landing zone, then a typed canonical core. Every core row carries nine provenance columns (source system, source id, record URL, raw payload hash, fetched-at, connector version, and more), eight of them NOT NULL at the schema layer, so any figure the system states can be walked back to the exact record it came from.

Three sources flow through one connector protocol into a raw JSONB landing layer and then a typed canonical core with provenance columns.
Fig 1 · three sources, one connector protocol, raw landing then canonical core. Every core row is traceable.

The chat pipeline: plan, execute, join, compose #

The chat is where most agent demos fall apart, so I rebuilt it. The first version was a ReAct loop: the model calls a tool, sees the result, decides the next move, and repeats. Complex questions needed three or four round-trips, which put p95 near 18 seconds. I replaced it with a fixed four-stage pipeline.

A four-stage pipeline: plan emits a typed DAG, execute fans tools out via asyncio.gather, join verifies or replans once, compose streams and cites.
Fig 2 · one planner call emits a typed task DAG, a deterministic executor fans the tools out, a joiner verifies, a composer streams.

Collapsing the loop to three fixed model hops with the tool fan-out done deterministically between them took p95 from about 18 seconds to a measured p50 of 3.49s and p95 of 4.91s against real Gemini. The planner never emits raw numbers, only a DAG of typed tasks that can reference each other.

Citation as an architectural contract #

The load-bearing claim is that the numbers are real, so citation is enforced in the architecture, not requested in a prompt. Metric queries only ever run through a semantic layer of eight metrics and eight dimensions whose compiler mandates a citation projection on every query, so a number cannot exist without its source rows attached. The composer emits placeholders, never digits; the orchestrator substitutes them inline; and a final verifier scans the shipped text with a numeral regex, raising a warning on any uncited literal.

a cited answer, on the wire · text
1
2
3
4
5
6
7
8
Q: post-RTO ROAS for the Meta 'Diwali-Prospecting' campaign, last 7d?

A: 1.8x [1], down from 3.1x gross [2] once RTO and freight
   are netted out. 34% of its COD orders returned [3].

   [1] core.ad_spend_daily + core.order  (query_hash a1f3…)
   [2] core.ad_spend_daily              (query_hash 9c02…)
   [3] core.shipment                    (query_hash 7be1…)

Agents that propose, never act #

On top of the read path sit three agents on one protocol, each propose-only. There is no HTTP client anywhere in the agents package and the run logger hard-codes dry-run, so the system physically cannot execute a write. Every run persists its reasoning, score, band, and expected rupee savings for a human to approve.

  • RTO Risk Flagger: on the order webhook; proposes WhatsApp confirm, downgrade-to-prepaid, or ship-as-is.
  • Meta Pauser: a six-hour cron; proposes pausing or halving budget on campaigns that are dead once attribution is RTO-adjusted.
  • Pincode COD Blocker: a daily cron; proposes blocking COD on the worst-offending pincodes, with a review gate.
A founder needs to argue with the score before they trust it. 'Pincode 110084 is 34% RTO over 87 orders' beats a black box they cannot poke at.

That is why the score is a linear weighted sum with three bands, not an XGBoost model. Explainability was the product requirement, not accuracy at the third decimal.

Built to scale to 10,000 merchants #

One tenant was the starting point; thousands was the target, so the scale story is in code, not slides. The first thing to break under onboarding waves is third-party rate limits, not webhook volume, so that is where the harness went.

  • A per-tenant Redis Lua-atomic token bucket absorbs rate limits (Shiprocket tokens expire every 240 hours and there is no bulk export).
  • A two-queue task system with SELECT FOR UPDATE SKIP LOCKED keeps a fresh merchant's 30k-row backfill from starving a live webhook.
  • Non-blocking webhook ingress writes to an inbox and returns 200 in about 4ms.
  • Hash partitioning by tenant from day one, so cell-based sharding later is a partition move, not a rewrite.

Where it breaks #

246 tests cover it, including an eval harness over twelve golden and ten red-team prompts for the citation contract. I also wrote a limitations doc that names the sharp edges before anyone else has to find them: the planner picks the wrong metric on a few percent of prompts, cross-source attribution depends on merchants configuring UTMs, and 10k load has a harness but was never run above about 100 tenants. I would rather state that plainly than pretend a system this size has none.

← PREVIOUS
Tessera
NEXT →
Supermarket Ops Agent