Skip to main content
Locked 2026-06-19. Spec: docs/superpowers/specs/2026-06-19-db-display-only-trust-anchor.md.

The claim

The Postgres database is display-only for the customer fund path. A compromise of the database — including the owner or service_rolecannot redirect a customer’s payment, because the checkout verifies the settlement destination on-chain, client-side, before the customer signs.
The platform is therefore not a custodian of routing, not merely of funds.
Never describe this as “unhackable.” Describe the mechanism and its conditions. The non-absolutist communications floor applies, and the audit is still pending.

The mechanism

The merchant pins their payout wallet in the embed code on their own siteCryptoCheckout.init({ settlementAnchor }) or ?anchor=0x…. That pin is the root of trust, and it lives somewhere the platform’s database cannot reach. Before the customer signs, the widget:
1

Reads the on-chain attestation

MerchantRegistry.effectiveRecipient(anchor) — an ownerless, msg.sender-scoped attestation the database cannot author.
2

Re-derives the pool address

From that chain-read recipient, using a byte-identical browser port of predictPoolAddress, pinned to the Solidity fixture vectors. Asserts it equals the served address.
3

Cross-checks

Connect rail: reads the deployed MerchantPool.recipient() and compares. Deposit rail: re-derives the forwarder from the verified pool.
4

Refuses on any mismatch

Terminal pool_mismatch state. Signing is blocked and the pay-to address is hidden, with a plain “configuration mismatch — do not pay” message.
The critical property: a self-consistent database tamper dies here. An attacker who swaps both the recipient and the pool address in the database still fails, because re-derivation anchors to the chain rather than to the served merchant_pools.recipient. The edge functions return a binding bundle — an account identifier plus committed configuration — which the client re-proves. The server response, like the database, is an untrusted hint. The pool itself is immutable and ownerless. Once funds land, the split to committed recipients is frozen in bytecode.

Conditions and limits

State these whenever the claim is made.

Server-side defence in depth

Shipped 2026-06-25. These do not replace the client check — they reduce the blast radius of a database compromise that the client would catch anyway.
  • Keeper re-resolves recipient via on-chain effectiveRecipient and gates deploy and sweep on the re-derived address.
  • Indexer re-derives the expected pool and forwarder and flags tamper_suspected, never reconciling an unanchored pool.
  • Database has an authorize-and-audit layer on fund routing: a commit_merchant_pool() security-definer function, a before-write guard, and an audit trigger.
Honest residuals, do not oversell. The database guard is authorize-and-audit, not service_role-proof — a direct-SQL service_role can set the guard’s own GUC. merchants.wallet_address and merchant_wallets inserts remain service_role-writable and audit-only. The client-side on-chain re-derivation stays load-bearing.
There was also a real bug worth remembering: the keeper’s anchor resolution returned null for every chain because the shared contracts module indexed bucket names rather than chain IDs when parsing the nested deployments file. Fixed 2026-06-25.

What this is not

The pool-deployment gate is not a security requirement. Verified against the client verification code: predictPoolAddress is pure CREATE2 arithmetic and re-derives counterfactually with no code needed; the only load-bearing chain read is against the per-chain singleton registry, not the merchant’s pool; the deployed-recipient() cross-check is optional and skips silently on failure; and effectiveRecipient is identity-default, so even an unattested merchant verifies correctly.Gate on pool deployment for commercial and connect-rail reasons. Do not justify it as required for this guarantee — that claim would be wrong.

Primitives