Policy bound risk gate for treasury wallets: allow, confirm, or block every tx with onchain proof
ChainShield Agent is a policy bound risk gate that sits in front of treasury wallets, multisigs, and agent-controlled keys, evaluating every outbound transaction intent before it ever reaches a signer. It is built to defend against the ways onchain money actually disappears in the wild: drainer-style approvals, swapped-recipient transfers, forbidden function selectors, and routine human-or-agent error.
Each request passes through a deterministic five-rule decision ladder - forbidden selector check, per-transaction ETH cap, 24-hour rolling outflow cap, allowed-destination list, and per-token approval cap - followed by a heuristic ERC-20 simulator that decodes transfer / transferFrom / approve calldata and projects balance deltas locally, without a network round-trip. The engine returns one of three verdicts - ALLOW, REQUIRE_HUMAN_CONFIRMATION, or BLOCK - plus a list of human-readable reasons and machine-readable rule keys, so a downstream agent or UI can render the same answer two ways.
Every verdict, and the policy that produced it, is serialized to JSON and anchored on 0G Storage on Galileo testnet, turning each decision into a tamper-evident, publicly verifiable record. When the engine BLOCKs a transaction, it fires a KeeperHub remediation workflow and an optional Discord webhook, so an operator is paged within seconds with the exact reason and the 0G anchor hash.
The full system is 100% TypeScript on Bun: Fastify 5 + Zod for the API, ethers v6 for chain wiring, the official @0gfoundation/0g-storage-ts-sdk for anchoring, and an Astro 6 frontend with a policy editor, an evaluate panel, and a timeline that renders every decision with a click-to-verify 0G anchor pill. Ninety tests pass in ~280ms; live anchor proofs are verified on Galileo. Sponsor adapters (0G, KeeperHub, Discord) are env-gated and degrade to in-memory fallbacks for local demos, so a judge can clone, bun install, and run the full pipeline in under a minute.
The full stack is 100% TypeScript on Bun 1.3 - no Node anywhere, including CI, where setup-bun@v2 reads .bun-version so local and remote runs are bit-for-bit aligned. The HTTP layer is Fastify 5 with @fastify/cors, validation is Zod at the API boundary only (parsed once at the edge, plain TS types flow through internals), chain wiring is ethers v6, persistence goes through @0gfoundation/0g-storage-ts-sdk, and tests use bun:test (ninety specs run in ~280ms). The frontend is a separate Bun workspace at web/ running Astro 6 with its own bun.lock; every astro command runs under bunx --bun astro <cmd> to bypass Astro's Node shebang - Astro 6 requires Node >=22.12 but Ubuntu CI ships Node 20, and running under Bun's runtime sidesteps the version mismatch entirely.
Internally, the server is composed around three interfaces - Store, Simulator, and PlaybookRunner - each with a real implementation and an in-memory/mock fallback. The composition root in server.ts reads env vars and wires the right pair: ZERO_G_PRIVATE_KEY swaps InMemoryStore for ZeroGStore, KEEPERHUB_API_KEY swaps MockRunner for KeeperHubRunner, NOTIFY_DISCORD_WEBHOOK adds a Discord channel. The decision engine itself doesn't know about any of them; it just sees the trait. The verdict ladder is monotonic by construction (BLOCK > REQUIRE_HUMAN_CONFIRMATION > ALLOW), with forbiddenSelectors short-circuiting before any other rule fires - that invariant is explicitly tested.
0G Storage is the proof half of the system. Every policy and every decision is serialized to JSON and anchored on Galileo testnet, and the resulting (rootHash, txHash) pair is injected at the serialization boundary by a withAnchor pattern so the core engine stays pure. Anchor writes are wrapped in a soft-failure helper that catches both ts-sdk error-tuple returns and thrown exceptions, so a Galileo hiccup never produces a 5xx; the local store still appends and the API responds with anchor: null. The Astro frontend renders an anchorPillHtml with a click-to-verify link to the Galileo explorer, XSS-hardened via escapeHtml for text and encodeURIComponent for the href, with adversarial payloads exercised in tests.
KeeperHub is the actuation half. On a BLOCK, KeeperHubRunner fires the configured workflow over REST with the org-level API key. KeeperHub's auth-failure bodies can include raw HTML, which originally bled into the UI through decision.reasons; we now route every error through a summarizeErrorBody helper that strips markup before persistence. A scripts/kh.sh bash wrapper reads .env.local so we can hit the API by hand to debug workflow IDs without restarting the server. Discord is a third channel - a one-line webhook adapter that pages an operator with the verdict, reason, and 0G anchor hash on every BLOCK.
The most notable shortcut is the heuristic ERC-20 simulator. Rather than spinning up a forked node or calling eth_call against a fork, it decodes transfer / transferFrom / approve calldata in pure TypeScript and projects balance deltas locally, returning a result in microseconds. Unknown selectors return success on purpose (the forbiddenSelectors rule handles the dangerous ones), and approve emits a string-tagged delta (+approval N) so the intent stays obvious in serialized output. We also pin live production anchor hashes (root + tx) as test constants in webFormat.test.ts so the renderer is exercised against real chain data rather than stubs.
CI runs typecheck on both workspaces (tsc --noEmit for the server, astro check for the web), all ninety tests, the Astro production build, and a byte-level emoji scan that fails the build if any banned codepoint sneaks into a tracked file - the no-emoji rule is enforced mechanically, not by review. Dependabot is grouped by area (deps, deps-web, actions, docker) so the inbox stays quiet, and ethers majors are pinned until the 0G SDK supports them. The whole pipeline - install, typecheck, test, build, scan - runs end-to-end in well under a minute, so a judge can clone the repo, bun install once at root and once in web/, and have the full stack live on :8787 (API) and :4321 (Astro) in a single command via `bun run dev.

