On 3 October we described the building block: a signed catalog snapshot that keeps discovery honest when a WooCommerce API is unreachable. This post is about what it took to put that snapshot into operation as a continuity layer, and what we measured when we broke a live store on purpose.
An AI shopping agent asks a federated catalog for "a gluten-free flour under €10". One of the best matches comes from a small WooCommerce store. At that exact moment, the store's REST API returns a 500: a hosting hiccup, a maintenance window, an anti-bot rule that just changed.
What should the network do?
There are two obvious answers, and both are wrong.
Drop the store. Simple and "safe", but it punishes the merchant for something that has nothing to do with their catalog. In agentic commerce, a store that disappears for an hour disappears from every answer generated during that hour. Discovery is not a page you can refresh later; it is a decision an agent makes now.
Serve whatever we cached. Convenient, but it quietly turns a cache into an authority. How old is it? Is it complete? Has the merchant withdrawn consent since? A cache has no answers to those questions, and an agent cannot tell the difference.
The tension is easy to state:
Availability without authority is unsafe. Authority without continuity is fragile.
At KaliCart we treated that gap as its own infrastructure problem: catalog continuity. This post describes how we designed it, what we measured, and what we got wrong along the way.
The Catalog Continuity Contract
The core idea is not a cache. It is a contract between the merchant and the network:
The merchant authorizes the network to temporarily serve the last complete, verified representation of its catalog when the live channel is unavailable, within explicit limits.
We hold that contract to four invariants:
- Provenance. Every snapshot is published by the merchant's own plugin and bound to a verified store identity. The network never builds a substitute catalog from scraping or guesswork.
- Completeness. A snapshot is either complete or it is not served. Partial catalogs silently mislead agents ("this store only sells three products").
- Bounded freshness. A snapshot has an expiry. Past it, the store is suspended, not "best-effort" served.
- Reversibility. An explicit consent revocation, a failed signature or identity verification, or freshness expiry cuts the path, and the network falls back to fail-closed.
Everything else in the design is the machinery that keeps those four promises under real-world failure.
What the merchant publishes
The KaliCart Bridge plugin (WordPress/WooCommerce) periodically publishes a static snapshot of the store's public catalog: a manifest plus content shards. The manifest carries a monotonic sequence number, a content hash, a completeness flag and a generation timestamp. The plugin announces each new generation through a signed heartbeat, so the network learns about it within minutes instead of discovering it by polling.
Two design choices matter here:
- Static files, not another API. When the dynamic stack is the thing that's failing (PHP workers, REST routing, a security plugin), plain files served by the web server often still work. The snapshot is deliberately boring.
- The build never touches the network. Generating a snapshot is local work; announcing it is a separate background job. A slow or failing network can delay a signal, never break a build.
The hot candidate: verify before the emergency
The naive version of a fallback downloads the snapshot when the outage happens. That is the worst possible moment: the merchant's server is already struggling, and you are about to make it serve the whole catalog to you.
Instead, the network keeps a hot candidate: the latest snapshot, already downloaded, verified against its manifest and staged, ready to be switched in. Verification happens on a calm day, not during the incident.
When the live API fails, switching source is a local operation. In our canary on a test store with 110 products:
- the source switch completed in 7.6 seconds from decision to serving;
- the merchant's server received zero snapshot requests during the switch;
- zero AI calls were made: every product was recognized as unchanged and reused its existing enrichment.
(These are measurements from a controlled test, not service-level promises.)
That last point deserves its own section.
Keep AI out of the control plane
Our catalog pipeline uses language models to enrich products: categories, attributes, normalized descriptions. It would be easy to let a source switch trigger re-enrichment "just to be safe". That would turn every outage into a bill, and every flapping API into a cost amplifier.
So we made a hard rule: switching between live and snapshot is deterministic and AI-free. Enrichment is keyed on the content that actually feeds it. If the snapshot carries the same product content as the live API, the cached enrichment is reused. We track unexpected AI calls as an explicit metric, and in the canary that number was zero.
The general principle: models can live in the data plane, never in the decision of which source is authoritative right now.
A state machine with hysteresis
Serving follows a small state machine:
A few properties keep it from becoming a liability:
- No reaction to a single blip. A store goes to snapshot only after several consecutive failed checks, spaced in time.
- Minimum dwell and a transition cap. A store that oscillates between up and down gets pinned to the snapshot until it has been healthy for a sustained window. Flapping is the enemy of trust: an agent that sees prices change source every five minutes cannot reason about them.
- Recovery is earned, not assumed. A successful health check does not flip the store back to live. Recovery runs a full, authoritative re-read of the live catalog and applies it atomically. Only then does the store return to live. If the re-read fails, the store stays on the snapshot that was still valid.
And the snapshot stays hot through the round trip. One of the bugs we found (more on that below) was a routine refresh destroying the standby right after recovery: the next outage would have fallen back to the slow path. The fix: keep the candidate as long as it is still the current generation, and re-arm it promptly after recovery when it is missing. In a second simulated outage, the switch came straight from the hot candidate, with no download.
Fail-closed, with the right semantics
"Fail-closed" sounds simple until you ask closed on what.
We ended up distinguishing four situations that look similar from the outside:
- The API is down but the plugin is there (5xx, timeouts, maintenance). That's an outage: serve the snapshot within its freshness window.
- WordPress answers, but the plugin is gone. It may be a temporary deactivation, an update in progress or recovery mode. The network treats absence alone as an outage, not as an explicit revocation: a previously verified hot snapshot may remain usable only within the consent and freshness bounds already in force, after which the store is suspended.
- The merchant explicitly revokes consent. That's immediate. No grace period, no snapshot.
- The snapshot itself expires or fails verification. Suspended, regardless of anything else.
Recovery starts symmetrically. In our test, a store under active monitoring was confirmed healthy again under two minutes after the plugin was reactivated. For stores checked less often, the reactivated plugin sends a signed heartbeat a couple of minutes later, and the network uses that signal to re-check the store right away instead of waiting hours for its regular schedule. Returning serving to live is a separate, guarded step: it still requires the full authoritative re-read and the state-machine conditions to pass.
The point is that "missing" and "revoked" are different facts, and a continuity layer has to keep them apart. Treating every absence as a revocation punishes merchants for routine maintenance. Treating every absence as consent forever ignores the merchant's control.
Not every release should cost the merchant a full re-read
A quieter lesson came from routine plugin releases. Our network used to force a full re-read of a store's catalog whenever the plugin version changed, to make sure every product carried the newest format. For one store with ~950 products, that meant about 50 minutes of work and load on the merchant's server, for a release that only changed lifecycle code.
We now separate the plugin's software version from the catalog data version it produces. For current Bridge versions that declare that data version, routine full re-reads are keyed to a data-format change rather than to every software release; legacy or unknown formats stay conservative. The work is not "free": it is simply no longer paid for releases that don't change catalog data.
How we built it: implementation, independent review, human decision
KaliCart is built by a very small team, with AI as a core part of the engineering loop. The method that worked for this project:
- one AI implements, with tests;
- a second AI reviews independently, and is expected to reproduce failures, not just comment on code;
- a human decides on scope, risk and rollout.
The reviews found real bugs that the implementation's own tests missed:
- the routine refresh that destroyed the hot candidate;
- a lost wakeup: a recovery signal could be dropped if another check held the same store at that moment. The first two fixes just moved the time limit; the third removed it entirely by making whoever records the first success responsible for the confirmation;
- cross-lane concurrency: two independent schedulers could probe the same store at once and skip a confirmation step. The fix was one probe per store at a time, across all processes.
We don't present two AIs as a guarantee. The value is in the independent reproductions and in a human who owns the trade-offs. One example: when a plugin is deactivated, briefly serving a still-valid snapshot is a risk we chose explicitly, as the lesser evil, and bounded in time. That's a decision, not a default.
Operational lessons
- Lifecycle callbacks must be boring. A deactivation hook that did "helpful" cleanup, including a hard rewrite-rules flush, reliably triggered a 500 on a host with a stale PHP opcode cache. The real problem was environmental, but our callback was the trigger. Now: no network calls, no heavyweight rebuilds in activation or deactivation paths; changes are deferred and applied softly on the next normal request.
- The obvious fix can be the trigger. "Regenerate permalinks" is the standard WordPress advice, and on that host it reproduced the failure. Measure before you prescribe.
- Canary with real failure, not mocks. We cut the live API of a test store at the edge, watched the network switch, released it, and watched recovery, twice, including the pinned path. Several of our assumptions only broke under that test.
What this is, and what it is not
The snapshot is not an authority for checkout, payment or final availability. The merchant's live store remains the source of truth for the purchase. What the continuity layer protects is discoverability: an agent can still find and describe the store's products, with clear provenance, while the live channel recovers.
Agentic commerce will not be built on APIs alone. Agents act on a store's products in real time and at scale, and every gap becomes a lost answer. The infrastructure underneath needs what the web learned from CDNs: continuity. Merchant-signed, bounded, verifiable and reversible.
That's the Catalog Continuity Contract. We think every agentic catalog will need one.
