diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 0000000..4187bf9
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,40 @@
+# `docs/` — project documentation
+
+> Everything that is not source code, organised so judges, contributors, and future-you can find what they need in one click.
+
+| File | Audience | What it covers |
+|---|---|---|
+| [`submission.md`](./submission.md) | ETHGlobal judges | One-pager: track, sponsors used, on-chain proof table, decision-engine summary, run instructions, test coverage |
+| [`demo-script.md`](./demo-script.md) | Recording team | Verbatim 7-beat walkthrough for the demo video |
+| [`architecture.md`](./architecture.md) | Maintainers + reviewers | System design, 8-step decision flow, post-hackathon Rust + Solidity roadmap |
+| [`deploy.md`](./deploy.md) | Anyone hosting it | $0 deploy walkthrough on Render + Cloudflare Pages, including CORS + keepalive config |
+| [`product-idea.md`](./product-idea.md) | Future-you | Original product framing notes from the planning session |
+| [`sponsors/`](./sponsors) | Maintainers + sponsors | Per-sponsor research notes — see [`sponsors/README.md`](./sponsors/README.md) |
+
+## Reading order
+
+```mermaid
+flowchart LR
+ A[../README.md
quick orientation] --> B[submission.md
judge one-pager]
+ B --> C[architecture.md
system design]
+ C --> D[sponsors/
sponsor research]
+ D --> E[demo-script.md
recording walkthrough]
+ E --> F[deploy.md
$0 hosting]
+```
+
+## Conventions
+
+- All docs are plain Markdown with optional Mermaid blocks.
+- Every doc that references a file uses a relative link so GitHub renders them and offline editors can follow them.
+- No emojis anywhere — the same project-wide rule the source code follows. CI scans these files too.
+- Live on-chain proof (rootHash, storage tx, block, gas) is duplicated in [`../README.md`](../README.md) and [`submission.md`](./submission.md) so judges can verify before clicking through.
+
+## Pointers
+
+| | |
+|---|---|
+| Project root | [`../README.md`](../README.md) |
+| Source | [`../src/`](../src) |
+| Tests | [`../tests/`](../tests) |
+| Frontend | [`../web/`](../web) |
+| Conventions for AI agents | [`../AGENTS.md`](../AGENTS.md) |
diff --git a/docs/sponsors/README.md b/docs/sponsors/README.md
new file mode 100644
index 0000000..3edc417
--- /dev/null
+++ b/docs/sponsors/README.md
@@ -0,0 +1,35 @@
+# `docs/sponsors/` — sponsor research notes
+
+> Per-sponsor research and integration notes captured during planning. Each file is the working spec the adapter was built against, kept after the build so the integration story is reproducible.
+
+| File | Sponsor | What it covers | Adapter |
+|---|---|---|---|
+| [`0g.md`](./0g.md) | 0G Labs | Storage SDK shape, Galileo testnet config, Indexer.upload semantics, faucet path, mainnet vs testnet contracts | [`../../src/memory/zeroGStore.ts`](../../src/memory/zeroGStore.ts) |
+| [`keeperhub.md`](./keeperhub.md) | KeeperHub | API key types, workflow execute endpoint, response shape variance, auth patterns | [`../../src/playbooks/keeperhub.ts`](../../src/playbooks/keeperhub.ts) |
+| [`gensyn-axl.md`](./gensyn-axl.md) | Gensyn | AXL local HTTP bridge model, MCP / A2A support, NAT-friendly mesh, bootstrap-node requirements | [`../../src/transport/axlGossip.ts`](../../src/transport/axlGossip.ts) |
+
+## Prize matrix
+
+| Sponsor | Prize pool | Status |
+|---|---|---|
+| 0G | `$15,000` | Adapter shipped, live anchor verified on Galileo testnet |
+| KeeperHub | `$5,000` | Adapter shipped, real workflow fired against live API |
+| Gensyn AXL | `$5,000` | Adapter shipped (decision gossip transport), MCP-style publish endpoint |
+| **Total addressable** | **`$25,000`** | All three adapters real, tested, env-gated, soft-failure |
+
+## How to add a new sponsor
+
+1. Add a research note `.md` in this folder — what it does, what its API looks like, what shape it expects.
+2. Pick the right trait seam: `Store`, `Simulator`, `PlaybookRunner`, `NotificationChannel`, or `GossipTransport`.
+3. Implement the real adapter in `src//`, mirroring an existing one (`KeeperHubRunner` for HTTP-based, `ZeroGStore` for SDK-based).
+4. Wire it into [`../../src/risk-gate/server.ts`](../../src/risk-gate/server.ts), env-gated so a missing key falls back to the in-memory or no-op impl.
+5. Add tests under `../../tests/.test.ts` — happy path + soft-failure + adversarial bodies.
+6. Update [`../../README.md`](../../README.md) sponsor section + this index.
+
+## Pointers
+
+| | |
+|---|---|
+| Parent | [`../README.md`](../README.md) |
+| Project root | [`../../README.md`](../../README.md) |
+| Wiring conventions | [`../../.claude/skills/sponsor-wiring/SKILL.md`](../../.claude/skills/sponsor-wiring/SKILL.md) |
diff --git a/scripts/README.md b/scripts/README.md
new file mode 100644
index 0000000..c29c73d
--- /dev/null
+++ b/scripts/README.md
@@ -0,0 +1,35 @@
+# `scripts/` — operator helpers
+
+> Small bash scripts used during development. Both honour `.env.local` so credentials never live on the command line.
+
+| File | What it does |
+|---|---|
+| [`dev.sh`](./dev.sh) | Boots the API on `:8787` (Bun watch mode) and the Astro frontend on `:4321` in parallel. Kills both on Ctrl-C. Wired to `bun run dev` |
+| [`kh.sh`](./kh.sh) | Bash wrapper for the KeeperHub REST API — reads `KEEPERHUB_API_KEY` and `KEEPERHUB_API_URL` from `.env.local`. Subcommands: `list`, `get `, `run `, `status `, `ping` |
+
+## `kh.sh` examples
+
+```sh
+./scripts/kh.sh ping # auth check
+./scripts/kh.sh list # workflows in the org
+./scripts/kh.sh get 8c12ujo1ax7b93w21updd
+./scripts/kh.sh run 8c12ujo1ax7b93w21updd '{"owner":"0xtreasury"}'
+./scripts/kh.sh status
+```
+
+The wrapper exists so workflow ids can be discovered and exercised without rerunning the full server, and so the org-key vs user-key distinction is centralised in one place rather than repeated in shell history.
+
+## Conventions
+
+- Every script reads `.env.local` if it exists; never echoes secrets.
+- Failures `exit 1` with a one-line reason; success is silent or prints structured output.
+- No `set -e` shortcuts that silently mask real failures — explicit checks for empty env vars and HTTP non-2xx.
+
+## Pointers
+
+| | |
+|---|---|
+| Parent | [`../README.md`](../README.md) |
+| KeeperHub adapter | [`../src/playbooks/keeperhub.ts`](../src/playbooks/keeperhub.ts) |
+| Sponsor research | [`../docs/sponsors/keeperhub.md`](../docs/sponsors/keeperhub.md) |
+| Env template | [`../.env.example`](../.env.example) |
diff --git a/src/README.md b/src/README.md
new file mode 100644
index 0000000..68fd1bf
--- /dev/null
+++ b/src/README.md
@@ -0,0 +1,46 @@
+# `src/` — ChainShield server
+
+> Fastify HTTP API that evaluates transaction intents against a deterministic policy engine, anchors every decision on 0G Storage, fires KeeperHub remediation on `BLOCK`, and gossips the verdict over the Gensyn AXL mesh.
+
+The whole server is plain TypeScript on Bun. Five trait-shaped seams (`Store`, `Simulator`, `PlaybookRunner`, `NotificationChannel`, `GossipTransport`) keep every external dependency replaceable — tests run against in-memory fakes; production wires real adapters from env.
+
+| Folder | What it is |
+|---|---|
+| [`core/`](./core) | Types, Zod schemas, `PolicyService`, `DecisionEngine`, EVM selector helpers |
+| [`memory/`](./memory) | `Store` interface, `InMemoryStore`, `ZeroGStore` (0G anchor adapter) |
+| [`simulator/`](./simulator) | `Simulator` interface + `HeuristicSimulator` (ERC-20 calldata decode + balance projection) |
+| [`playbooks/`](./playbooks) | `PlaybookRunner` interface, `KeeperHubRunner`, `WebhookChannel`, `CollectorChannel` |
+| [`transport/`](./transport) | `GossipTransport` interface, `AxlGossipTransport`, `NoopGossip` |
+| [`risk-gate/`](./risk-gate) | Fastify `app.ts` + `server.ts` composition root |
+| [`cli/`](./cli) | `demo.ts` — four canonical scenes against the live API |
+
+## Request flow
+
+```mermaid
+flowchart LR
+ Caller[Wallet / treasury client] -->|POST /evaluate| App
+ App[risk-gate/app.ts
Fastify route] --> Engine
+ Engine[core/engine.ts
5-rule ladder] --> Sim[simulator/
HeuristicSimulator]
+ Engine --> Store[memory/
ZeroGStore]
+ Engine -->|BLOCK| Run[playbooks/
KeeperHubRunner]
+ Engine -->|BLOCK| Gossip[transport/
AxlGossipTransport]
+ Engine -->|BLOCK| Notify[playbooks/
WebhookChannel]
+```
+
+## Hard rules
+
+1. Verdict ladder is monotonic: `BLOCK > REQUIRE_HUMAN_CONFIRMATION > ALLOW`. A rule may only escalate, never de-escalate.
+2. `forbiddenSelectors` is checked before any other rule and short-circuits to `BLOCK` with risk 95.
+3. Defensive guards (`invalidIntentValue`, `invalidApprovalCap`) escalate to `REQUIRE_HUMAN_CONFIRMATION` (risk 70) instead of throwing.
+4. Every decision is persisted via `Store.appendDecision` exactly once.
+5. `reasons[]` is human-readable English; `rulesMatched[]` is machine-readable rule keys. Keep them in sync.
+6. Sponsor adapters fail soft — a Galileo hiccup or KeeperHub outage never returns 5xx.
+
+## Related
+
+| | |
+|---|---|
+| Tests | [`../tests/`](../tests) — 109 specs across 13 files |
+| Composition root | [`risk-gate/server.ts`](./risk-gate/server.ts) |
+| Frontend | [`../web/`](../web) — Astro 6, separate Bun workspace |
+| Conventions | [`../AGENTS.md`](../AGENTS.md) |
diff --git a/src/cli/README.md b/src/cli/README.md
new file mode 100644
index 0000000..b112841
--- /dev/null
+++ b/src/cli/README.md
@@ -0,0 +1,36 @@
+# `src/cli/` — demo runner
+
+> A four-scene CLI that drives the live API end-to-end. Used for the recording and as a smoke test before submission.
+
+| File | Role |
+|---|---|
+| [`demo.ts`](./demo.ts) | Boots a fresh policy via `POST /policies`, then runs four `POST /evaluate` calls covering every verdict path. Exits `0` if the verdict and reasons match the expected shape, `1` otherwise. |
+
+## Scenes
+
+| # | Intent | Expected verdict | Rule that fires |
+|---|---|---|---|
+| 1 | Small transfer to allowlisted vault | `ALLOW` | (all rules pass) |
+| 2 | Transfer above the per-tx ETH cap | `BLOCK` | `maxTransferEth` |
+| 3 | `approve(spender, MAX_UINT256)` | `BLOCK` | `forbiddenSelectors` (or `approvalCapByToken`) |
+| 4 | Transfer to an off-allowlist destination | `REQUIRE_HUMAN_CONFIRMATION` | `allowedDestinations` |
+
+## How to run
+
+```sh
+# 1. boot the API (in another terminal)
+bun run dev
+
+# 2. drive the four scenes
+bun run demo
+```
+
+The demo uses `INTERNAL_API_BASE` if set, otherwise defaults to `http://127.0.0.1:8787`. Output prints the verdict, risk score, matched rules, and the `anchor.rootHash` if 0G is wired.
+
+## Pointers
+
+| | |
+|---|---|
+| Parent | [`../README.md`](../README.md) |
+| API surface | [`../risk-gate/app.ts`](../risk-gate/app.ts) |
+| Recording walkthrough | [`../../docs/demo-script.md`](../../docs/demo-script.md) |
diff --git a/src/core/README.md b/src/core/README.md
new file mode 100644
index 0000000..7149f76
--- /dev/null
+++ b/src/core/README.md
@@ -0,0 +1,61 @@
+# `src/core/` — types, schemas, engine
+
+> The heart of ChainShield. Pure TypeScript with no I/O — every external dependency is injected as a trait so the engine stays unit-testable in microseconds.
+
+| File | Role |
+|---|---|
+| [`types.ts`](./types.ts) | All exported types: `TxIntent`, `Policy`, `Decision`, `Verdict`, `SimulationResult`, `BalanceDelta`, `ApprovalDelta`, `PlaybookRun`, `LlmReasoning` |
+| [`schemas.ts`](./schemas.ts) | Zod schemas at the API boundary — `policyInputSchema`, `evaluateRequestSchema`. Parsed once at the edge; plain TS types flow through internals |
+| [`policyService.ts`](./policyService.ts) | Policy CRUD with version bumping. Zod-validates input, persists to `Store`, returns the canonical record |
+| [`engine.ts`](./engine.ts) | `DecisionEngine` — the 5-rule ladder + simulator integration + remediation dispatch |
+| [`selectors.ts`](./selectors.ts) | EVM 4-byte selector helpers (`selectorOf`, `decodeUint256`) + curated selector constants (`ERC20_APPROVE`, `ERC20_TRANSFER`, …) |
+
+## Decision ladder
+
+```mermaid
+flowchart TD
+ A[Intent in] --> B{forbiddenSelectors?}
+ B -->|yes| Z[BLOCK risk 95]
+ B -->|no| C{maxTransferEth?}
+ C -->|over| Z
+ C -->|under| D{maxDailyOutflowEth?}
+ D -->|over| Z
+ D -->|under| E{allowedDestinations?}
+ E -->|off-list| F[REQUIRE_HUMAN_CONFIRMATION risk 60]
+ E -->|on-list| G{approvalCapByToken?}
+ G -->|over| Z
+ G -->|under| H{simulator revert?}
+ F --> H
+ H -->|revert| I[REQUIRE_HUMAN_CONFIRMATION risk 70]
+ H -->|success| J[ALLOW]
+ J --> K[persist + anchor]
+ I --> K
+ Z --> L[persist + anchor + remediation]
+```
+
+## Invariants (do not break)
+
+1. The verdict ladder is **monotonic** — rules can only escalate.
+2. `forbiddenSelectors` short-circuits before any other rule.
+3. `maxTransferEth` and `approvalCapByToken` produce `BLOCK` with risk ≥ 90.
+4. `maxDailyOutflowEth` reads the timeline; only non-blocked rows count toward the rolling sum.
+5. `allowedDestinations` downgrades `ALLOW` to `REQUIRE_HUMAN_CONFIRMATION` (risk 60). It cannot promote to `BLOCK` on its own.
+6. Defensive guards escalate to `REQUIRE_HUMAN_CONFIRMATION` instead of throwing.
+7. `reasons[]` and `rulesMatched[]` stay in sync.
+
+## Tests
+
+| | |
+|---|---|
+| 5-rule ladder + guards | [`../../tests/engine.test.ts`](../../tests/engine.test.ts) |
+| Simulator integration + revert escalation | [`../../tests/engineSimulation.test.ts`](../../tests/engineSimulation.test.ts) |
+| Playbook + notifications + AXL gossip on `BLOCK` | [`../../tests/engineRemediation.test.ts`](../../tests/engineRemediation.test.ts) |
+| Policy CRUD + version bumping | [`../../tests/policyService.test.ts`](../../tests/policyService.test.ts) |
+
+## Pointers
+
+| | |
+|---|---|
+| Parent | [`../README.md`](../README.md) |
+| Wired by | [`../risk-gate/server.ts`](../risk-gate/server.ts) |
+| Selector reference | [`../../.claude/skills/selector-decode/SKILL.md`](../../.claude/skills/selector-decode/SKILL.md) |
diff --git a/src/memory/README.md b/src/memory/README.md
new file mode 100644
index 0000000..cd19698
--- /dev/null
+++ b/src/memory/README.md
@@ -0,0 +1,50 @@
+# `src/memory/` — Store layer
+
+> Pluggable persistence behind a single trait. Tests use the in-memory impl; production wires the 0G anchor adapter.
+
+| File | Role |
+|---|---|
+| [`store.ts`](./store.ts) | The `Store` interface (`putPolicy`, `getPolicy`, `listPolicies`, `appendDecision`, `listDecisions`, optional `getAnchor`) and the `AnchorRecord` type |
+| [`memoryStore.ts`](./memoryStore.ts) | `InMemoryStore` — pure in-process maps + arrays. Used in tests and as the fallback when `ZERO_G_PRIVATE_KEY` is unset |
+| [`zeroGStore.ts`](./zeroGStore.ts) | `ZeroGStore` — uploads each policy and decision JSON to 0G Galileo via `@0gfoundation/0g-storage-ts-sdk`, caches the local copy, exposes anchor `(rootHash, txHash)` per id |
+
+## Anchor flow
+
+```mermaid
+flowchart LR
+ PS[PolicyService.put] --> ZG[ZeroGStore.putPolicy]
+ ZG -->|in-memory write returns ~50ms| Caller
+ ZG -.->|background| Sched[scheduleAnchor]
+ Sched --> Try[tryAnchor]
+ Try --> SDK["Indexer.upload(MemData(json))"]
+ SDK --> Galileo[(0G Galileo
storage indexer)]
+ Galileo -->|"{rootHash, txHash}"| Anchors[(anchors map)]
+ Anchors --> Get[getAnchor by id]
+```
+
+## Soft-failure contract
+
+- `Indexer.upload` returns either an error-tuple `[result, Error|null]` or throws — `tryAnchor` handles both paths.
+- Two response shapes: single `{ rootHash, txHash, txSeq }` and fragmented `{ rootHashes[], txHashes[], txSeqs[] }` (>4 GB). `tryAnchor` normalises both.
+- Failure is logged (`logger.warn`) and the anchor map simply lacks an entry — the API still serves the response with `anchor: null`.
+- Background uploads never block the API hot path; tests use the optional `waitForAnchor(id)` helper to settle pending uploads deterministically.
+
+## Tests
+
+| | |
+|---|---|
+| Anchor on write, soft-failure, empty-result handling | [`../../tests/zeroGStore.test.ts`](../../tests/zeroGStore.test.ts) |
+| Anchor surfacing on policy + decision API responses (real `ZeroGStore` + `buildApp` e2e) | [`../../tests/apiAnchor.test.ts`](../../tests/apiAnchor.test.ts) |
+| Multi-tenant decision isolation by client id | [`../../tests/clientIsolation.test.ts`](../../tests/clientIsolation.test.ts) |
+
+## Live proof
+
+A real policy was anchored on Galileo testnet — see the rootHash + storage tx table at the top of [`../../README.md`](../../README.md).
+
+## Pointers
+
+| | |
+|---|---|
+| Parent | [`../README.md`](../README.md) |
+| Wired by | [`../risk-gate/server.ts`](../risk-gate/server.ts) — picks `ZeroGStore` when `ZERO_G_PRIVATE_KEY` is set |
+| Sponsor research | [`../../docs/sponsors/0g.md`](../../docs/sponsors/0g.md) |
diff --git a/src/playbooks/README.md b/src/playbooks/README.md
new file mode 100644
index 0000000..c970410
--- /dev/null
+++ b/src/playbooks/README.md
@@ -0,0 +1,51 @@
+# `src/playbooks/` — remediation + notifications
+
+> When a verdict is `BLOCK`, ChainShield does more than say "no" — it fires an automated remediation playbook and pages every configured notification channel. Both layers are pluggable behind small interfaces.
+
+| File | Role |
+|---|---|
+| [`runner.ts`](./runner.ts) | The `PlaybookRunner` and `NotificationChannel` interfaces, plus `MockRunner` for tests |
+| [`keeperhub.ts`](./keeperhub.ts) | `KeeperHubRunner` — fires KeeperHub workflows via `POST /api/workflow/:id/execute` with bearer auth, scrubs HTML error pages |
+| [`notifier.ts`](./notifier.ts) | `WebhookChannel` (Discord-shaped embed by default, custom shapes via `contentTemplate`) and `CollectorChannel` (test-only in-memory recorder) |
+
+## Remediation flow
+
+```mermaid
+flowchart LR
+ Engine[DecisionEngine.handleRemediation] --> R{remediation.onBlock?}
+ R -->|empty| N
+ R -->|ids[]| RR[KeeperHubRunner.run id 0]
+ RR -->|ok| Decision[playbookTriggered set]
+ RR -->|throws| Next[try id 1, id 2, ...]
+ Next -->|all fail| Decision2[reasons append failure]
+ Decision --> N{notifyChannels?}
+ Decision2 --> N
+ N -->|empty| Done
+ N -->|name list| CH[for each registered channel
WebhookChannel.notify]
+ CH --> Done[return decision]
+```
+
+## Hard rules
+
+- A failing playbook **never** affects the verdict or persistence — failures are pushed into `decision.reasons` (truncated to 256 chars) and the engine continues.
+- Notification failures are silently swallowed — paging cannot down-convert a `BLOCK` to anything else.
+- `KeeperHubRunner` returns the first response field it finds in priority `runId → id → executionId` and falls back to `"unknown"` so the JSON shape stays stable.
+- HTML error bodies (auth-failure pages, 404s) are detected by `summarizeErrorBody` and replaced with `(html error page)` before they reach `decision.reasons` or the UI.
+
+## Tests
+
+| | |
+|---|---|
+| `MockRunner` invocation tracking + forced failures | [`../../tests/playbooks.test.ts`](../../tests/playbooks.test.ts) |
+| `KeeperHubRunner` happy path, response-shape fallback, HTML scrub, body truncation, URL encoding | same file |
+| `WebhookChannel` Discord embed shape + custom template + non-2xx error | same file |
+| Engine integration: trigger on `BLOCK`, fall-through on failure, notification fan-out | [`../../tests/engineRemediation.test.ts`](../../tests/engineRemediation.test.ts) |
+
+## Pointers
+
+| | |
+|---|---|
+| Parent | [`../README.md`](../README.md) |
+| Wired by | [`../risk-gate/server.ts`](../risk-gate/server.ts) — picks `KeeperHubRunner` when `KEEPERHUB_API_KEY` is set |
+| Helper script | [`../../scripts/kh.sh`](../../scripts/kh.sh) — `list / get / run / status / ping` against the live KeeperHub API |
+| Sponsor research | [`../../docs/sponsors/keeperhub.md`](../../docs/sponsors/keeperhub.md) |
diff --git a/src/risk-gate/README.md b/src/risk-gate/README.md
new file mode 100644
index 0000000..c22c9b0
--- /dev/null
+++ b/src/risk-gate/README.md
@@ -0,0 +1,60 @@
+# `src/risk-gate/` — Fastify API + composition root
+
+> The HTTP surface and the wiring that decides which adapter to use for each trait. Reads env vars once at boot, swaps real adapters in place of the in-memory fallbacks, and starts listening.
+
+| File | Role |
+|---|---|
+| [`app.ts`](./app.ts) | `buildApp(deps)` — Fastify instance, CORS layer, Zod-validated routes for `/policies`, `/policies/:id`, `/evaluate`, `/timeline`, `/health`. Anchor metadata is injected at the serialization boundary by `withAnchorPolicy` / `withAnchorDecision` |
+| [`server.ts`](./server.ts) | The composition root — reads env, picks `Store` / `PlaybookRunner` / `GossipTransport`, registers notification channels, calls `defaultEngine`, starts listening on `:8787` |
+
+## Adapter selection
+
+| Env var | When set | Real adapter | When unset | Fallback |
+|---|---|---|---|---|
+| `ZERO_G_PRIVATE_KEY` | non-empty | `ZeroGStore` (anchors on Galileo) | unset | `InMemoryStore` |
+| `KEEPERHUB_API_KEY` | non-empty | `KeeperHubRunner` (real workflows) | unset | `MockRunner` |
+| `NOTIFY_DISCORD_WEBHOOK` | set | adds `discord` channel via `WebhookChannel` | unset | only `collector` channel registered |
+| `AXL_BASE_URL` | non-empty | `AxlGossipTransport` (publish over mesh) | unset | `NoopGossip` |
+| `WEB_ORIGIN` | comma list | parsed via `parseOriginEntry` (literal or `/regex/`) | unset | defaults to `http://127.0.0.1:4321,http://localhost:4321` |
+
+## Routes
+
+```mermaid
+flowchart LR
+ C[Caller] -->|GET /health| H[200 OK]
+ C -->|POST /policies| PP[PolicyService.put
+ anchor]
+ C -->|GET /policies/:id| GP[Store.getPolicy
+ anchor]
+ C -->|GET /policies| LP[Store.listPolicies
+ anchor each]
+ C -->|POST /evaluate| EV[DecisionEngine.evaluate
+ anchor]
+ C -->|GET /timeline| TL[Store.listDecisions
+ anchor each]
+```
+
+## Anchor injection
+
+`ZeroGStore.getAnchor(id)` is **optional** on the `Store` interface. `app.ts` calls it via optional chaining, so `InMemoryStore` (which has no anchor concept) needs no changes — the API simply omits the `anchor` field.
+
+```ts
+// inside app.ts
+function withAnchorDecision(d: Decision): WithAnchor {
+ const anchor = store.getAnchor?.(d.id);
+ return anchor ? { ...d, anchor } : d;
+}
+```
+
+## Tests
+
+| | |
+|---|---|
+| End-to-end API flow against an in-memory store | [`../../tests/api.test.ts`](../../tests/api.test.ts) |
+| Anchor surfacing on policy + decision responses (real `ZeroGStore` + `buildApp`) | [`../../tests/apiAnchor.test.ts`](../../tests/apiAnchor.test.ts) |
+| CORS allowlist + Cloudflare Pages preview-domain regex | [`../../tests/cors.test.ts`](../../tests/cors.test.ts) |
+
+## Pointers
+
+| | |
+|---|---|
+| Parent | [`../README.md`](../README.md) |
+| Engine | [`../core/engine.ts`](../core/engine.ts) |
+| Frontend that calls these routes | [`../../web/`](../../web) |
+| Containerised deploy | [`../../Dockerfile`](../../Dockerfile), [`../../docker-compose.yml`](../../docker-compose.yml) |
+| $0 hosting walkthrough | [`../../docs/deploy.md`](../../docs/deploy.md) |
diff --git a/src/simulator/README.md b/src/simulator/README.md
new file mode 100644
index 0000000..d6f3bc6
--- /dev/null
+++ b/src/simulator/README.md
@@ -0,0 +1,52 @@
+# `src/simulator/` — pre-signature simulation
+
+> A heuristic ERC-20 simulator that decodes calldata locally and projects balance deltas without a network round trip. Returns a result in microseconds, so the engine can escalate borderline intents without a fork.
+
+| File | Role |
+|---|---|
+| [`simulator.ts`](./simulator.ts) | The `Simulator` interface — `simulate(intent)` returning a `SimulationResult` (`success`, `revertReason?`, `balanceDeltas[]`, `approvals[]?`, `gasUsed?`) |
+| [`heuristic.ts`](./heuristic.ts) | `HeuristicSimulator` — decodes `transfer` / `transferFrom` / `approve` calldata and projects deltas; treats native ETH and unknown selectors as success |
+
+## What it covers
+
+```mermaid
+flowchart LR
+ I[TxIntent] --> S{selector}
+ S -->|transfer 0xa9059cbb| T[debit from / credit to]
+ S -->|transferFrom 0x23b872dd| TF[debit from / credit to + spender]
+ S -->|approve 0x095ea7b3| AP[emit ApprovalDelta]
+ S -->|none / native ETH| ETH[debit from / credit to value]
+ S -->|unknown| OK[success: no deltas]
+ T --> R[SimulationResult]
+ TF --> R
+ AP --> R
+ ETH --> R
+ OK --> R
+```
+
+## Why heuristic, not fork
+
+| | Fork-based simulator | Heuristic simulator |
+|---|---|---|
+| Round trip | `eth_call` against fork (~hundreds of ms) | None — pure calldata decode |
+| Coverage | Whole EVM | ERC-20 transfer / transferFrom / approve + native ETH |
+| Edge cases | Fee-on-transfer, blacklists, pause | Caught by the `forbiddenSelectors` rule before the simulator runs |
+| Cost | RPC fees + node maintenance | Free |
+| Latency budget | Out of scope for hot-path API | Microseconds — fits inside the `~50 ms` API target |
+
+The simulator is one of six rules in the engine ladder; `forbiddenSelectors` already blocks the dangerous tokens, so the heuristic only needs to model the happy paths well.
+
+## Tests
+
+| | |
+|---|---|
+| Calldata decode + balance deltas + typed `approvals[]` | [`../../tests/simulator.test.ts`](../../tests/simulator.test.ts) |
+| Engine integration + revert escalation | [`../../tests/engineSimulation.test.ts`](../../tests/engineSimulation.test.ts) |
+
+## Pointers
+
+| | |
+|---|---|
+| Parent | [`../README.md`](../README.md) |
+| Consumed by | [`../core/engine.ts`](../core/engine.ts) — `runSimulator()` |
+| Selector reference | [`../core/selectors.ts`](../core/selectors.ts) |
diff --git a/src/transport/README.md b/src/transport/README.md
new file mode 100644
index 0000000..0636000
--- /dev/null
+++ b/src/transport/README.md
@@ -0,0 +1,56 @@
+# `src/transport/` — peer-to-peer decision gossip
+
+> When ChainShield issues a `BLOCK`, the verdict needs to reach every co-operating gate without a centralised relay. This folder publishes each blocked decision over the Gensyn AXL agent mesh — soft-failure, no SDK lock-in, language-agnostic.
+
+| File | Role |
+|---|---|
+| [`axlGossip.ts`](./axlGossip.ts) | `AxlGossipTransport` — `POST ${AXL_BASE_URL}/api/v1/mcp/publish` with a `{ topic, payload: { decision, policy } }` body. Default topic `chainshield.decisions` |
+| [`noopGossip.ts`](./noopGossip.ts) | `NoopGossip` — used when `AXL_BASE_URL` is unset, so the rest of the system behaves identically without an AXL node running |
+
+## Gossip flow
+
+```mermaid
+flowchart LR
+ Engine[DecisionEngine
handleRemediation] -->|BLOCK only| GT[GossipTransport.broadcast]
+ GT --> Pub[POST /api/v1/mcp/publish
topic: chainshield.decisions]
+ Pub --> Local[(AXL local bridge
:9002)]
+ Local --> Mesh[(AXL mesh
MCP / A2A)]
+ Mesh --> P1[Co-operating gate 1]
+ Mesh --> P2[Co-operating gate 2]
+ Mesh --> P3[Co-operating gate N]
+```
+
+## Why AXL
+
+- **Local HTTP bridge at `:9002`** — language-agnostic, no SDK, drop-in for any agent stack.
+- **Built-in MCP / A2A support** — structured agent-to-agent messages without re-implementing libp2p.
+- **No central relay** — co-operating gates form a mesh; one offline node never breaks the rest.
+- **NAT/firewall friendly + end-to-end encrypted** by default.
+
+## Soft-failure contract
+
+`GossipTransport.broadcast` is required to **never throw**. Implementations log internally on failure and return. The engine awaits the call so blocked decisions reach the mesh before the API response returns; because the contract guarantees no throws, this awaiting is safe.
+
+| Failure mode | Behaviour |
+|---|---|
+| 5xx response | `logger.warn` with status + summarised body; no throw |
+| Network error (`ECONNREFUSED`, DNS, etc.) | `logger.warn` with the truncated message; no throw |
+| HTML error page in the body | Collapsed to `(html error page)` so markup never reaches the logs |
+| Oversized JSON error body | Truncated to 200 chars + `…` |
+
+## Tests
+
+| | |
+|---|---|
+| Happy path, default + custom topic, soft-failure on 5xx, soft-failure on network error, HTML scrubbing, `NoopGossip` no-op | [`../../tests/axlGossip.test.ts`](../../tests/axlGossip.test.ts) |
+| Engine integration: gossip fires on `BLOCK`, never on `ALLOW` / `REQUIRE_HUMAN_CONFIRMATION` | [`../../tests/engineRemediation.test.ts`](../../tests/engineRemediation.test.ts) |
+
+## Pointers
+
+| | |
+|---|---|
+| Parent | [`../README.md`](../README.md) |
+| Wired by | [`../risk-gate/server.ts`](../risk-gate/server.ts) — picks `AxlGossipTransport` when `AXL_BASE_URL` is set, else `NoopGossip` |
+| Engine hook | [`../core/engine.ts`](../core/engine.ts) — broadcast invoked from `handleRemediation` after the playbook runner |
+| Sponsor research | [`../../docs/sponsors/gensyn-axl.md`](../../docs/sponsors/gensyn-axl.md) |
+| AXL docs | |
diff --git a/tests/README.md b/tests/README.md
new file mode 100644
index 0000000..c761011
--- /dev/null
+++ b/tests/README.md
@@ -0,0 +1,51 @@
+# `tests/` — unit + integration coverage
+
+> 109 specs across 13 files, all green in `~340 ms`. Every external dependency is faked at the trait boundary; live anchor hashes are pinned as test constants so the renderer is exercised against real chain data.
+
+```sh
+bun test # all 109 specs
+bun test tests/engine.test.ts # one file
+bun test --watch # watch mode
+bun test --coverage # coverage report
+```
+
+## File index
+
+| File | Domain |
+|---|---|
+| [`api.test.ts`](./api.test.ts) | Risk-gate Fastify API end-to-end |
+| [`apiAnchor.test.ts`](./apiAnchor.test.ts) | Anchor surfacing on policy + decision responses (real `ZeroGStore` + `buildApp`) |
+| [`axlGossip.test.ts`](./axlGossip.test.ts) | `AxlGossipTransport` happy path, soft-failure (5xx, network error, HTML body), `NoopGossip` |
+| [`clientIsolation.test.ts`](./clientIsolation.test.ts) | Multi-tenant decision isolation by client id |
+| [`cors.test.ts`](./cors.test.ts) | CORS allowlist + Cloudflare Pages preview-domain regex + suffix-attack rejection |
+| [`engine.test.ts`](./engine.test.ts) | 5-rule decision ladder + `invalidIntentValue` / `invalidApprovalCap` defensive guards |
+| [`engineRemediation.test.ts`](./engineRemediation.test.ts) | Playbook trigger, fall-through on failure, notification fan-out, AXL gossip on `BLOCK` |
+| [`engineSimulation.test.ts`](./engineSimulation.test.ts) | Simulator integration + revert escalation |
+| [`playbooks.test.ts`](./playbooks.test.ts) | `MockRunner`, `KeeperHubRunner`, `WebhookChannel`, `CollectorChannel` |
+| [`policyService.test.ts`](./policyService.test.ts) | Policy CRUD + version bumping + Zod schema rejection |
+| [`simulator.test.ts`](./simulator.test.ts) | `HeuristicSimulator` calldata decode + balance deltas + typed `approvals[]` |
+| [`webFormat.test.ts`](./webFormat.test.ts) | Astro renderer: `shortHash`, `anchorPillHtml`, `escapeHtml` adversarial XSS |
+| [`zeroGStore.test.ts`](./zeroGStore.test.ts) | Anchor on write, soft-failure, empty-result handling, single + multi response shapes |
+| [`helpers.ts`](./helpers.ts) | Shared fixtures: `TREASURY` / `COLD_VAULT` / `ATTACKER` / `TOKEN` addresses, `makePolicy`, `makeIntent`, `approveCalldata` |
+
+## Patterns to mirror
+
+| Pattern | Where it lives |
+|---|---|
+| Mock fetcher injected into HTTP-based adapters | `tests/playbooks.test.ts` (KeeperHub), `tests/axlGossip.test.ts` |
+| Real adapter against a fake indexer | `tests/zeroGStore.test.ts` (`IndexerLike` injected) |
+| Live production hashes pinned as test constants | `tests/webFormat.test.ts` (`FULL_ROOT`, `FULL_TX`) |
+| Fresh `InMemoryStore` per spec | every `tests/engine*.ts` and `tests/api*.ts` |
+| Deterministic `now()` + `idGen()` injected | `tests/engineRemediation.test.ts`, `tests/engineSimulation.test.ts` |
+
+## CI gates
+
+The same `bun test` runs in CI on every PR to `main` and every push, alongside `tsc --noEmit`, `astro check`, the Astro production build, and a byte-level emoji scan. A red test fails the pipeline; nothing merges without all checks green.
+
+## Pointers
+
+| | |
+|---|---|
+| Source under test | [`../src/`](../src) |
+| Bun test docs | |
+| Coding conventions | [`../AGENTS.md`](../AGENTS.md) |