Overview
Shop Support Agent answers customer messages for a Shopify store. It looks up orders and products through the Admin GraphQL API, answers from the store's own policy file, and hands anything it should not decide to a person. A console around it lets a team use it, and each new store is a row, not a new build.
It ran live against a Shopify development store. A later slice routes sales, quote and service enquiries to an owner queue and, only with the visitor's explicit yes, captures them in Zoho CRM. That part was written from Zoho's v8 documentation and tested against a stand-in server.
Key Features
- The order is the design. A legal threat, a chargeback, a safety report or a request for a person is matched in code before the model sees the message. The model can add an escalation and never remove one.
- Facts come from the store, not the model. A grounding guard refuses any reply with a figure not in the facts, a commitment the agent cannot make, or an outcome the facts do not state. Orders are identity-checked against the email on the message.
- Customer memory is a hint, never a fact. Notes go stale, only the customer's own words are kept, and one pointer to the last order is verified live each time.
- Enquiry routing and lead capture. Intent goes to an owner queue. Consent is code: the agent asks once and only an explicit yes in the visitor's own words counts. The lead, a follow-up task and a note are written once per email per day under an idempotency key, and a failed capture never changes the reply.
- An embeddable chat widget for two websites. One script tag with a Shadow DOM panel, two tenants with their own theme, greeting, allowed origins and owner queue, a signed server-issued conversation id, rate limits and size caps, text-only rendering, and Yes and No buttons for the consent question. It works with the keyboard and a screen reader, and a real-browser suite drives both demo sites.
- A Zoho MCP server. Tools over the lead desk (find a lead by email, create one with consent recorded, add a note, create a follow-up task, list queues) with strict schemas, credentials only from the environment, and a confirm flag and an idempotency key on every write. A small Zia Agents client was written only from the saved Zoho pages.
- A WhatsApp lane, a Discord staff ticket and a Notion log. A WhatsApp message reaches the agent and the reply goes back, with the Meta signature checked on every request and a retried delivery answered once. An escalated turn posts one staff message in Discord with Take and Resolve buttons, the interactions endpoint verifies an Ed25519 signature on every click, and each ticket is one row in a Notion data source that a click moves from Open to Taken to Resolved. The Notion log looks the ticket up before every create and retry, because Notion has no idempotency key. None of them keeps the customer's words or contact details. All three were written from the vendors' documentation and tested against stand-in servers.
- Appointment booking on Google Calendar. A visitor asks for an appointment, the agent offers the next three free times from the calendar's free and busy answer, takes a pick and a name and books one event. It reads free and busy again right before the write, derives the event id from the slot and visitor, and looks an event up after a lost answer instead of sending it twice. A calendar that Google answers with an error entry is never treated as free, and every reply is written in code from the calendar's own times. It was tested against a stand-in written from Google's documentation and ran once on a temporary calendar that was deleted afterwards.
- Three model providers. The agent answers through Groq, the Claude API or IBM watsonx.ai, chosen by one setting. The Claude and IBM providers were written from each vendor's documentation and tested against stand-in servers that follow them.
- Isolation in the database. One Postgres, forced row-level security per store, and an application role that is not a superuser.
Technical Achievements
The suite is 1183 tests, 11 of them in real Chromium on both demo sites, and a mutation harness deletes 429 guards one at a time, each of which has to turn a test red. A seeded property test found a real bug in the lead desk: a second enquiry with the same words but a different intent was swallowed as a duplicate note. It is fixed and pinned. CI runs the engine suite and the console suite against a Postgres service, and the console lint fails on any warning.
What I Learned
A guard only counts once a test fails without it. The first mutation run left three mutants alive: two tests that did not exercise the guard they named, and one line that was redundant. I fixed the tests and removed the line, instead of defending it.
Tech Stack
Agent: Python, a scripted model in tests, a grounding guard, an escalation floor Integration: Shopify Admin GraphQL, Zoho CRM v8 (Lead upsert, Task, Note, OAuth refresh, Australian region domains), n8n, WhatsApp Cloud API, Discord bot, Notion API, Google Calendar API Console: FastAPI, Next.js, TypeScript, PostgreSQL with row-level security Testing: pytest, Playwright, a mutation harness of 429 guards Infrastructure: Docker, GitHub Actions