# Letting AI agents shop

By [Anubhav Choubey](https://anubhavchoubey.com/) · Backend & Systems · 2026-11-04 · 4 min read

> A Go service I built that lets AI agent platforms browse a store, build product feeds and hand checkout to a human. Two lanes and one contract.

```stats
Go service · two lanes, one contract · reads from a replica · the shopper always pays
```

## The problem

AI assistants are starting to shop for people. A shopper asks for "a waterproof jacket under a budget", and an agent platform wants to search a store, build a basket and get the person to checkout. For a managed hosting platform for online stores, that raises three questions:

- How does an agent read a store's catalogue without slowing the store down?
- How do agent platforms get an accurate product feed?
- How does a basket built by a machine become an order paid for by a person?

My answer to the last one shaped everything else: **the agent never pays**. There is no order, payment, refund or coupon tool. They are absent, not disabled. The agent builds a basket and hands over a link, and the shopper finishes on the store itself.

## Two lanes and a contract

The work had two parts that ship separately: a shared Go service, and a plugin that runs inside each store. I planned it as two lanes with a contract between them: versioning, request signing, the handful of calls the plugin makes to the service, and the format of the hand-off link.

The contract came with **fixtures**, not just prose. The service lane shipped a fake service and a set of test vectors for the hand-off link: valid, expired, tampered and edge cases. One vector uses timestamps so far apart that subtracting them overflows a 64-bit integer, so the opener has to compare, never subtract. The plugin lane opens every vector in its own language and gets identical results. Each side built against a fake of the other, and joint checkpoints on a real store proved they fit.

```callout tip
Ship contract fixtures, not only contract docs. Two teams can read the same paragraph two ways. They cannot disagree about a test vector.
```

## Its own database and job runner

The service has its own MySQL database, its own identity that can reach only its own instances, and a default-deny network policy. Nothing it does needs the store's credentials.

Migrations are **additive-only by allowlist**. A failed deploy rolls back pods, not the schema, so a migration that drops a column cannot be undone by rolling back. There are no foreign keys on purpose: a cascade would turn small bounded deletes into one huge one.

Background work runs on a small job runner. Jobs are claimed per store and per kind with `SKIP LOCKED`, leased, renewed and handed back on shutdown. A claim counts as a failure until the run says otherwise, so a job that crashes its pod is retried with backoff instead of looping.

```go
// a claim is a failure until proven otherwise
job := claim(ctx, store, kind)
defer job.ReleaseAsFailed()
if err := run(ctx, job); err == nil {
    job.MarkDone()
}
```

Kill switches per store and per channel skip work rather than fail it.

## Reading the store from a replica

Every read comes from the store's analytics replica, through a SELECT-only database user. An agent's search never reaches the live store.

- **Change detection** runs hourly. Some product tables have no "updated at" column, so prices and stock are compared as digests page by page.
- **Catalogue lookup** answers search and cart-line validation from the replica, in a connection pool sized to the read user's limit.
- **The feed builder** produces a product feed hourly, then spot-checks a sample against the store's live cart API. Too many differences or missing rows hold the build, and the last good feed stays live.

```text
agent platform ──▶ service ──read──▶ replica
                     │
                     ├─ basket built via the store's cart API
                     ▼
                hand-off link ──▶ shopper's browser ──▶ checkout
```

If a store displays prices differently from how it stores them, the feed build is skipped: the price then depends on the shopper's location, which a replica cannot know. An honest gap beats a wrong price.

Agents reach the service through a gateway that verifies signed requests and signs its replies. A basket with items always ends the same way: "this needs a human", plus a link. Order events flow back to agent platforms as webhooks, and an older event never arrives after a newer one.

## Stats and alerts

The service records per-agent stats off the request path, so counting never slows a shopper down. Alerts are built from log lines, and the tests produce the real lines and commit them as fixtures. The alert filters are tested against those fixtures, so a renamed log field breaks a test, not an alert. Every per-store alert names the store, marked "unverified" when the name came from a URL before the signature was checked.

## What I'd do differently

- **Settle the contract before the first service code.** Parts of the service were built against a draft and then moved onto the agreed contract. Waiting a little longer would have saved a whole alignment change.
- **Run the real integration early.** An integration test against a real store caught a session bug that a full set of green unit tests could not see. That test belongs at the start.

## Takeaway

Let the agent do the browsing and the basket. Keep the payment with the person. With that one rule fixed, the rest is ordinary engineering: a contract with fixtures, reads from a replica, jobs that fail safely, and alerts tested like code.
