Concepts

Verifiability

A signed audit chain that lets you prove the IPS in effect on June 15 — and every guardrail decision since — was what we say it was, without trusting our database. Design draft.

Why this exists

The agent platform’s value is the policy layer. Today, that policy lives in our Postgres database — meaning anyone with DB write access could in principle alter what your IPS said yesterday, or change a past guardrail decision’s stored reason. We don’t do that, and we have access controls + audit log on the database itself, but you can’t verify any of that. You have to take our word for it.

Two classes of customer eventually need more than trust:

  • Compliance teams who have to evidence to a regulator that the policy in effect on a specific date was the one they approved.
  • Crypto-native customers whose mental model expects to verify state cryptographically rather than via vendor attestation.

The design below addresses both without the complexity of putting policy content on a public chain.

The model: a signed hash chain

Every event that defines or applies the policy lands in an append-only policy_events log:

  • ips.created / ips.amended — a new IPS version goes live
  • standing_rule.set / standing_rule.revoked
  • authority.set — an authority-map override
  • drawdown_rule.set
  • guardrail.decision — every block / allow the policy layer returns
  • key.minted / key.revoked / key.rotated — with a hash of the per-key policy, never the secret
  • policy.drift_detected / policy.drift_cleared — every time your portfolio crosses into or out of an IPS band breach
  • broker.trade_observed — every fill we sync from your custodian, with source: ‘ac_initiated’ or ‘user_initiated’ so the chain captures trades you placed on Gemini directly without going through AC. Schwab observation lands when the transaction-sync processor moves past stub.

Each row is shaped like this:

policy_events row
{
  id: string,                  // uuid
  userId: string,              // who the policy applies to
  eventType: string,           // one of the types above
  payload: unknown,            // event body (no secrets)
  occurredAt: string,          // ISO 8601 timestamp
  prevHash: string,            // 0x0 for the first event
  hash: string,                // sha256(prevHash + canonicalJson(payload) + occurredAt)
  signature: string,           // ed25519(hash) by AC's signing key
  signerKeyId: string,         // which AC pubkey to verify against
}

The hash links each event to its predecessor — change one row and every subsequent hash stops matching. The signature makes those hashes non-repudiable: an AC private key signed this exact byte sequence at this time.

AC’s signing keys

  • Ed25519 — small signatures, fast verification, no curve choices to defend.
  • Rotated quarterly. Each key has a public key id (signerKeyId), a valid-from / valid-to window, and is published in a public, signed /api/agent/v1/signing-keys manifest that we also include in the chain.
  • Private keys held in HSM. The signer service is the only system component with access. Compromise of the API process does not expose the key.

Reading the chain

Two new endpoints:

  • GET /api/agent/v1/policy/events — paginated cursor-based feed of your policy_events rows, with the full hash + signature on each. Scope: read.
  • GET /api/agent/v1/policy/snapshot — a point-in-time materialized view: “here is your policy as of timestamp T”, with the event id of the last event applied so a verifier can re-walk from genesis. Scope: read.

The verification client

We’ll ship a verifier as a CLI (npx @advisors-crypto/verify) and a library function in both SDKs. It does three things:

  • Walks the events you fetched and re-computes every hash from prevHash + payload + occurredAt.
  • Verifies each signature against the signer key valid atoccurredAt, using the published signing-key manifest.
  • Returns either a { ok: true, head: hash } envelope or the first row where verification broke, with the reason.
Verifier usage
import { verifyPolicyChain } from '@advisors-crypto/verify';

const events = await ac.getPolicyEvents();
const result = await verifyPolicyChain(events);
if (!result.ok) {
  throw new Error(`Chain broken at ${result.eventId}: ${result.reason}`);
}
console.log('Verified head:', result.head);

Why this is not on-chain (yet)

Two concrete reasons we are deferring on-chain checkpoints:

  • Privacy. Even a hash leaks timing — “this customer had an IPS change at 16:42” — and on a public chain that timing is observable forever by any analyst. The off-chain signed chain limits visibility to people who can read your /policy/events feed.
  • No additional guarantee for the common case. The threat model that on-chain solves is “AC and a customer’s legal team both lie to a regulator simultaneously.” That’s a real but narrow scenario; for everything else, a signed off-chain chain already prevents AC from quietly rewriting history.

When we ship the v0.5+ on-chain extension, the model is a Merkle root of the day’s policy_events hashes published to a Base contract once daily. That gives you third-party verifiability — “Base block 12M says AC’s root on 2026-06-15 was 0xabc...” — for a few cents per day. We will not put policy content on-chain.

Rollout

  • Phase 1 — Plumbing. policy_events table, signer service, hash + signature on every event we already emit (IPS changes, guardrail decisions, key mutations). No new UI yet.
  • Phase 2 — Read API + verifier. The two endpoints above plus the verification client. CLI ships via npm. Customers can manually run npx @advisors-crypto/verify and confirm.
  • Phase 3 — In-app verification. “Verify this snapshot” button on the /agent#policyview that runs the verifier in-browser and shows green.
  • Phase 4 (optional) — On-chain checkpoints. Daily Merkle root publish to a Base contract. The verifier gains an --with-chain flag that cross-references.

Non-goals

  • Not a blockchain. Events live in Postgres; the chain is in the prevHash column. We aren’t building consensus, only an append-only signed log.
  • Not a zero-trust system. You still trust AC to run the signer correctly and to publish the signing-key manifest honestly. Phase 4 reduces this to trust in Base + the manifest publication mechanism, not elimination.
  • Not policy enforcement. Enforcement is still guardrail.service.ts; the chain proves what we said we did, not that we did it. The two need to match — and that’s what audit reconciliation in phase 2 verifies against the existing audit log.

Open questions

  • How do we handle backfilled events when this ships — do we start the chain at “today” or reconstruct from existing audit + IPS history?
  • How do enterprises with their own KMS plug their own signing key alongside AC’s for dual-attestation?
  • What’s the right retention? policy_events can’t prune the way agent_api_calls does — but it grows forever otherwise.
Last updated 2026-06-15