Skip to content

02 — Architectural View

Altitude: components and the seams between them. This page says what the runnable and deployable parts are, which module is the single sanctioned path for each job, where state lives, and where the trust boundaries fall. Physical placement is 03; the wire-level connector and port tables are 04.

Diagram source: diagrams/architecture-view.drawio.

Component decomposition

The platform is five deployable units and one on-chain estate, plus a shared-package layer that exists specifically to stop duplication of things that must not drift.

flowchart TB
    subgraph Client ["Client — the only place member keys exist"]
        SPA["React SPA<br/>frontend/ · Vite"]
        MA["Mini-app packages<br/>untrusted, IPFS-served"]
        NAT["Capacitor shells<br/>iOS / Android"]
        SPA -->|host object only| MA
        NAT -.->|wraps| SPA
    end

    subgraph Shared ["packages/ — single sources of truth"]
        IT["@fairwins/intent-types<br/>34 structs · 7 domains"]
        AC["@fairwins/assistant-contract<br/>prompt + tool defs"]
        FC["finops-catalogue<br/>every money path"]
    end

    subgraph Gateway ["relay-gateway — policy gateway, holds no key"]
        ID["origin lock + identity tiering"]
        MOD["modules: intent · paymaster · polymarket<br/>perps · bitcoin · news · memberApi · x402 · fees"]
        ID --> MOD
    end

    subgraph Engines ["Submission engines"]
        OZ["OZ Relayer<br/>+ redis"]
        ALTO["alto bundler<br/>EntryPoint v0.6 · chain 137 only"]
    end

    subgraph Aux ["Auxiliary"]
        MCPS["MCP server<br/>dependency-free, cannot sign"]
        FX["FinOps exporter<br/>read-only by construction"]
    end

    subgraph Chain ["On-chain estate — 8 EVM chains + Bitcoin"]
        REG["WagerRegistry + Intents facet"]
        MM["MembershipManager"]
        POOLS["Wager / Funding pool factories"]
        FEE["FeeRouter"]
        ROUT["Bridge / Liquidity routers"]
        ACCT["Passkey account + paymaster"]
        GUARD["SanctionsGuard · SafePolicyGuard v1/v2"]
        MAR["MiniAppRegistry · CallsignRegistry"]
    end

    SPA -->|"reads: JSON-RPC direct"| Chain
    SPA -->|"writes: self-submit, always available"| Chain
    SPA -->|"optional rails + vendor reads"| Gateway
    Gateway --> Engines
    Engines -->|"pays gas"| Chain
    MCPS -->|"member's own token"| Gateway
    IT --- SPA
    IT --- Gateway
    AC --- SPA
    AC --- MCPS
    FX -.->|scrape| Gateway

Read the diagram for what it omits: there is no arrow from the gateway to a member's key, no arrow from the gateway into escrow, and no arrow from a mini-app to anything but the host object. Those absences are the architecture.

Unit responsibilities

Unit Language / runtime Holds a key? Optional? Responsibility
SPA (frontend/) React + Vite Member keys, client-side only No — it is the product Every member surface; all signing; all direct chain reads
Mini-app packages (frontend/miniapps/) Built separately, frozen at an immutable CID No Yes Third-party/first-party apps executing inside the host object
Capacitor shells iOS / Android native wrappers Bridges to the platform authenticator Yes Native passkey ceremony, Ledger BLE, app lock, deep links
relay-gateway (services/relay-gateway/) Node ESM No member key; KMS-signs paymaster ops Yes — every flow self-submits Policy gateway: origin lock, identity tiering, screening, quotas, killswitch; read-proxy for vendors with no CORS; member API; x402 paywall
OZ Relayer + redis Container Platform gas key (KMS) Yes Submits relayed intents and pays their gas
alto bundler Container, v1.2.7 Per-chain executor EOA Yes ERC-4337 bundling, EntryPoint v0.6, chain 137 only
MCP server (services/mcp-server/) Node, dependency-free by policy No — cannot sign, cannot pay Yes Exposes the member API to the member's own agents
FinOps exporter (services/finops-exporter/) Node No — no signer, no write route Yes Prometheus metrics for every catalogued money path
On-chain estate Solidity Escrow itself No All value custody and settlement

Two structural constraints explain more of the codebase than any design document: the 24 KB contract code limit, which is why WagerRegistry is two facets behind one proxy sharing a single storage definition; and the fact that frontend/src has ~2,966 extensionless imports while the gateway is Node ESM, which is why shared code had to be extracted into packages/ with explicit exports before the EIP-712 tables could stop being duplicated.

0. Findings that matter before the tables

# Finding
F1 🔶 resolveCredentialManager (FE/lib/passkey/credentials.js:171) is not exported — it is module-private and reached only as the default of the deps.credentials parameter of createCredential/getAssertion (:334, :409). CLAUDE.md names it as the seam; the enforced seam is actually the two ceremony functions. Stronger than documented.
F2 🔶 Two whole subsystems have shipped seams that CLAUDE.md does not mention: spec 106 gateway caller auth (GW/identity/, ROUTE_TABLE) and spec 107 keyed RPC access (GW/access/, FE/lib/network/issuedAccess.js). Both are first-class trust/seam boundaries.
F3 🔶 CLAUDE.md states perps ships no in-app execution (FR-018). Code contains a full spec 083 position-management path: config/perps.js#perpsManageEnabled/perpsManageFeatureEnabled (default OFF via VITE_PERPS_MANAGE_ENABLED), hooks/usePerpsOrders.js, hooks/usePerpsTrade.js, lib/perps/venues/{gains,gmx}.js, lib/perps/attestation.js. Read-only is now the flag-off state, not the only state.
F4 🔶 CLAUDE.md cites specs/103-capacitor-channels/; the directory on disk is specs/102-capacitor-channels/ (colliding with 102-multisig-chain-abstraction). 103-funding-pools is correct.
F5 🔶 scripts/secrets/registry.js:4 self-identifies as spec 088; CLAUDE.md attributes workstation secrets to spec 097. specs/088-instant-acting-accounts is a different feature. One of the two is a mis-citation.
F6 screeningChainIds() lives in FE/lib/screening/sources.js:185, not in config/networks.js (where CLAUDE.md's list of chain-roster helpers otherwise sits). isLocalOnlyChain lives in config/contracts.js:560, also not networks.js.
F7 Specs with no seam entry in CLAUDE.md at all: 086 account-cards, 088 instant-acting-accounts, 092 multi-chain-activity, 098 acting-account-purchase, 099 network-status-miniapp, 100 passkey-solana, 101 passkey-zcash, 106, 107. Code exists for 100 (FE/lib/solana/).

1. SEAM REGISTER

1.1 Native shell (spec 102-capacitor)

Seam Single source of truth for Callers Forbidden to bypass Gate / test Evidence
FE/lib/native/runtime.js#getRuntime / isNativeRuntime / nativeCapability The one runtime + capability read; three-state, available only when the bridging plugin confirmed itself every native-adjacent seam (lifecycle, deepLinks, credentials, ledgerAdapter), NativeCapabilityNotice reading window.Capacitor / plugin globals directly; fabricating available FE/test/native/runtime.test.js, supportFloor.test.js runtime.js:32,44,83
FE/lib/native/lifecycle.js#subscribeAppHidden App-hidden ⇒ engage app-lock, in native shell lib/applock/ consumers own appStateChange listener; INERT on web by construction ({native = isNativeRuntime()}) FE/test/native/lifecycle.test.js lifecycle.js:23
FE/lib/native/deepLinks.js#pathForIncomingUrl / subscribeDeepLinks Tenant-origin URL → in-app path app bootstrap accepting a non-tenant origin; hand-parsing appUrlOpen FE/test/native/deepLinks.test.js deepLinks.js:33,56
FE/lib/native/ledgerBleTransport.js#openNativeBleTransport (+ buildFrames, createFrameAssembler, LedgerNativeBleTransport) Ledger APDU framing over BLE in the native shell — one hw-transport rung lib/hardware/ledgerAdapter.js only importing @capacitor-community/bluetooth-le from UI code FE/test/native/ledgerBleRung.test.js ledgerBleTransport.js:34-36,49,73,113,195
FE/lib/native/nativeCredentials.js#nativeCredentialManager The credentials-shaped adapter over @capgo/capacitor-passkey, incl. PRF salt pre-encode / result decode credentials.js#resolveCredentialManager only passing a Uint8Array extension through the plugin (JSON-cloned ⇒ mangled) FE/test/native/passkeyBridge.test.js nativeCredentials.js:47,60,97,118
scripts/native/sync-native-config.js Version/identity fields in both shells (written from tenant manifest + scripts/release/version.js) release pipeline hand-editing shell config npm run check:native-versions (scripts/native/check-native-versions.js) package.json script
scripts/native/nativeCsp.js Native meta CSP, derived from infra/.../nginx.conf native prepare widening script-src; editing the native CSP by hand FE/test/native/nativeCspParity.test.js test file present

1.2 Passkey / account identity

Seam Single source of truth for Callers Forbidden to bypass Gate / test Evidence
FE/lib/passkey/credentials.js#createCredential / getAssertion (internal resolveCredentialManager) WebAuthn ceremony execution + which credential manager (web vs native) runs it; ceremony timeouts (CEREMONY_TIMEOUT_MS 120s, PINNED_… 30s); the fairwins.passkey.credentials.v1 record set every passkey flow (sign-up, sign-in, PRF key unwrap, cross-device) calling navigator.credentials directly; branching on native-ness at the call site FE/test/passkey/*, FE/test/native/passkeyBridge.test.js credentials.js:171,298,333,408
FE/lib/passkey/accountLookup.js#resolveAccounts / verifyAccountForKey Which account a passkey controlsResolution has 4 shapes, only resolved carries an address (resolved/noneFound/unverified/notController) sign-in flow; recovery surfaces deriving the address and returning it anyway; treating unverified as none-found; ownerIndex = 0 by assumption; unbounded waits (RESOLUTION_DEADLINE_MS 20s) ⚠️ UNVERIFIED — no FE/test/passkey/accountLookup*.test.js found; tests exist for bundlerTransport, deviceLossWarning, sendBatchSponsorship only accountLookup.js:24,40,51,56,65,74,127,188
FE/lib/passkey/prfKeys.js The PRF-derived master-seed blobs (fairwins.passkey.wrappedSeeds.v1) Bitcoin/Solana derivation, legacy vault passkey unlock deriving key material anywhere else ⚠️ UNVERIFIED gate prfKeys.js:24
FE/lib/bitcoin/derivation.js#deriveBtcSeed / addressAt BTC key derivation constants (BTC_HKDF_INFO = 'fairwins-btc-seed-v1', BIP84/86 paths) — wallet-breaking if changed lib/bitcoin/wallet.js, send.js, psbt.js a second HKDF info / path table FE/lib/bitcoin/__tests__/derivation.test.js derivation.js:42,84,99,144
FE/lib/solana/derivation.js#deriveSolanaKeypair Solana key derivation from the same master seed (SOLANA_COIN_TYPE 501, 3 schemes) lib/solana/send.js FE/lib/solana/__tests__/ derivation.js:29,82

1.3 Custody (Protect)

Seam Single source of truth for Callers Forbidden to bypass Gate / test Evidence
FE/lib/custody/writeRail.js#resolveWriteRail / requireWriteRail "Can this session sign a vault action on this chain?" — signer checked first, passkey rail only where isPasskeySupported(chainId), NONE otherwise with an actionable reason hooks/useVaultProposals.js, Queue UI, vault action callbacks branching on loginMethod === 'passkey'; offering a button that will throw inside sendPasskeyBatch; conflating unavailable-rail with "view-only" FE/test/custody/writeRail.test.js writeRail.js:25,42,72 (signer-first at :45-47)
FE/lib/custody/policyV2.js#matchPreview (+ classifyPayload, scopeEquals, analyzeShadowing, previewPolicyV2, encodeSetRules) Client-side twin of on-chain first-match-governs policy matching; rule encoding; V2 limits (MAX_RULES 16, MAX_APPROVERS 8, MAX_TARGETS 16) PolicyPanelV2, RuleComposer, RuleList a second matcher; changing preview without the Solidity FE/test/custody/policyV2.test.js + shared scenarios with the Solidity suite policyV2.js:22-25,191,208,234,280,308,368
FE/lib/custody/vaultRulesConfig.js#realizeRules The ONE semantic rules config → realized on-chain rule array, per chain createflow/RulesSheet.jsx, CreateVaultFlow putting policy into a multichain deployment initializer (byte-different ⇒ different CREATE2 address) FE/test/custody/vaultRulesConfig.test.js vaultRulesConfig.js:18-21,29,61,151
FE/components/custody/createflow/ (CreateVaultFlow.jsx + TypeSheet/NetworksSheet/RulesSheet/DoneSheet + createFlowModel.js) Vault creation: chain-independent spec-043 initializer + per-vault saltNonce; creation record Protect ▸ On chain creating per-network from a different flow; a "pick a network" prompt FE/test/custody/CreateVaultFlow.test.jsx dir listing
FE/lib/custody/vaultCreationRecords.js The immutable creation record (replay input for "Add a network"), userStorage key vault_creation_records, synced createflow, deriveNetworkStatus mutating a record FE/test/custody/vaultCreationRecords.test.js :16
FE/lib/custody/vaultReferences.js The only client-side custody data store: (chainId,address) vault references + labels, key custody_vault_references, synced useCustodyVaults keying the store by address (view-only is vaultGroups) FE/test/custody/vaultGroups.test.js, useCustodyVaults.multichain.test.jsx vaultReferences.js:1,9
FE/lib/custody/describeProposal.js Decoded proposal chips/rows — null over a guess Queue inventing a decode FE/test/custody/describeProposal.test.js dir
FE/hooks/useVaultQueueAcrossChains.js Per-chain four-state queue read (read/unreadable/not-configured/not-supported), partial totals named VaultQueueView summing across chains without naming the missing one FE/test/custody/useVaultQueueAcrossChains.test.jsx hooks listing
FE/lib/custody/submitAsActiveAccount.js Identity-first submit (member vs vault ⇒ proposal) vault actions, mini-app host wallet.submit submitting as the member while "operating as" a vault FE/test/custody/submitAsActiveAccount.test.js dir

1.4 Hardware wallets (spec 085)

Seam Single source of truth for Callers Forbidden to bypass Gate / test Evidence
FE/lib/hardware/adapters.js#connectHardware (+ detectTransports, ledgerTransportKind, vendorAvailability) The ONE vendor seam — Ledger/Trezor SDKs lazy-loaded behind it; transport selection incl. the BLE rung connectAccount.js, HardwareSigner, AddHardwareWalletSheet importing a vendor SDK from UI code; surfacing a raw SDK message (must go through HW_ERROR_CODES/describeHardwareError in hardware/errors.js) FE/test/hardware/transports.test.js, stagingRegressions.test.js adapters.js:27,30,41,59,76,97
FE/lib/hardware/connectAccount.js#connectHardwareAccount Reconnect = re-derive the saved path and match the saved address, else refuse Protect ▸ Off chain trusting the saved address without re-derivation FE/test/hardware/connectAccount.test.js connectAccount.js:17
FE/lib/hardware/hardwareAccountsStore.js Public device metadata only ({address,vendor,path,label,addedAt}), key hardware_accounts, synced hardwareAccounts.js facade storing key material / xpub / device id FE/test/hardware/hardwareAccountsStore.test.js :11 + syncedObjects.js:205
FE/lib/hardware/hardwareSigner.js Physical-confirmation signing + recover-and-verify before broadcast vault/transfer write paths broadcasting an unverified device signature FE/test/hardware/hardwareSigner.test.js dir

1.5 Network / RPC (specs 069, 107)

Seam Single source of truth for Callers Forbidden to bypass Gate / test Evidence
FE/lib/network/rpcEndpoints.js#resolveRpcEndpoints (+ getRpcUrlForChain, authHeadersFor, describeRpcRoute, probeRpcEndpoint) Endpoint precedence member override → build default; credential goes in a header, primary endpoint only utils/rpcProvider.js#makeReadProvider, wagmi.js transports hand-building a provider from NETWORKS[chainId].rpcUrl; writing a key into a URL FE/test/network/rpcEndpoints.test.js, rpcFailover.test.js, rpcProvider.endpoints.test.js rpcEndpoints.js:28,37,42,63,116,121,144
FE/utils/rpcProvider.js#makeReadProvider / getReadProvider Every ethers read provider (quorum-1 FallbackProvider; useEndpointsRevision in memo deps) all read hooks, screening, estate reads, readProviderFor new JsonRpcProvider(NETWORKS[..].rpcUrl) same as above rpcProvider.js:110,199
FE/lib/network/endpointStore.js (ENDPOINTS_PREF_KEY = 'network_endpoints', CSP_RPC_GRANTS) Member endpoint overrides; the connect-src grant list NetworkPanel syncing endpoints to backup; drifting from both nginx configs FE/test/network/endpointStore.test.js, FE/test/nginxCspConnectSrc.test.js endpointStore.js:21,36
FE/lib/network/issuedAccess.js#ensureIssuedAccess / issuedFetchFor / issuedAccessState (spec 107, undocumented in CLAUDE.md) Short-lived keyed-read credential lifecycle; state subscription read providers that can use keyed capacity reading keyed endpoints without the issued token; treating a refusal as an error surface (it means "fall back to public") FE/test/network/issuedAccess.test.js, issuedClient.test.jsx issuedAccess.js:55,71,93,176,194

1.6 Address screening (spec 021 amendment)

Seam Single source of truth for Callers Forbidden to bypass Gate / test Evidence
FE/lib/screening/sources.js#screeningSourcesFor / screeningChainIds (+ CHAINALYSIS_ORACLES, ISSUER_FREEZE_LISTS, SOURCE_KINDS) The source registry — FairWins SanctionsGuard, Chainalysis oracle (Base at a different address), Circle isBlacklisted / Tether isBlackListed; roster = cohort minus isLocalOnlyChain screenEstate.js adding an unverified row; including a local-only chain FE/test/screening/sources.test.js, screeningRoster.test.js sources.js:57,64,79,185,197
FE/lib/screening/verdict.js#deriveVerdict (+ countReadings, describeVerdict, worstVerdict, verdictOnChain) ONE word from readings: flagged on any hit, screened only when unreadable === 0, else partial, unscreened when nothing answered. Never stored. screenEstate, ScreeningPill, ScreeningStatusBar green on a list that did not answer; treating no-source as clear FE/test/screening/verdict.test.js verdict.js:25,36,53,76,158
FE/lib/screening/screenEstate.js#screenAddressAcrossEstate The cohort sweep: per-source failure isolation, DEFAULT_DEADLINE_MS 8_000, never rejects, providers from readProviderFor useEstateScreening, useEstateScreeningMany a background poll; a provider built by hand FE/test/screening/screenEstate.test.js screenEstate.js:31,110,180
FE/hooks/useAddressScreening Per-chain live read that gates a submission on the chain value moves on (the contract repeats this read) submit paths (FR-013/032) using it to render the advisory pill for a book of contacts (it can only answer for the wallet's chain) FE/test/screening/* useAddressScreening.js:34
FE/hooks/useEstateScreening / useEstateScreeningMany (+ NO_SOURCE) The advisory estate verdict under every address field / the book & contact picker; getVerdictOn third answer no-source AddressScreenNotice, ScreeningPill, address book using the per-chain hook for book rows FE/test/screening/ScreeningPill.test.jsx, ScreeningStatusBar.test.jsx; e2e MS-06 (no-chain tier must be amber) useEstateScreening.js:36,87,89

1.7 Message signing / verify (spec 084)

Seam Single source of truth for Callers Forbidden to bypass Gate / test Evidence
FE/lib/verify/verifyMessage.js#verifyMessage Offline, synchronous signature check; three verdicts valid/invalid/unverifiable (VERIFY_STATUS); canCheckOnChain marks the one escalable outcome VerifySection.jsx, verifySignedMessage giving it a chain/provider; making it async; promoting an ECDSA mismatch to a negative when the on-chain leg could not run FE/test/verify/verifyMessage.test.js; fixtures once in FE/test/fixtures/signedMessages.js verifyMessage.js:48,168 (plain function, no await)
FE/lib/verify/verifyMessage.js#verifyOnChain / checkErc1271 The network escalation (ERC-1271, ERC1271_MAGIC) — a timeout is not a forgery explicit member escalation only rendering an RPC timeout as invalid same verifyMessage.js:42,90,246
FE/lib/verify/signMessage.js Signing; refused while operating as a vault VerifySection signing under a "vault" label with the member's own key FE/test/verify/signMessage.test.js dir

1.8 Legacy recovery (spec 062)

Seam Single source of truth for Callers Forbidden to bypass Gate / test Evidence
FE/lib/recovery/legacyKeys.js#legacyKeyVault(account) Per-account CRUD facade over the store; encrypt/decrypt (PBKDF2_ITERATIONS 650000, AES-GCM), passkey-PRF variant Recovery section persisting/transmitting/logging a cleartext secret FE/test/recovery/legacyKeys.test.js, legacyKeysPasskey.test.js legacyKeys.js:32,152,180,229,269,298
FE/lib/recovery/legacyRecoveredKeysStore.js The store itself — key legacy_recovered_keys, synced (ciphertext only), single source of truth legacyKeyVault only a second store; a parallel merge FE/test/recovery/legacyRecoveredKeysStore.test.js store.js:6,11; syncedObjects.js:167
FE/lib/recovery/legacyKeys.js#sweepAllAssets (+ quoteAllAssets, supportedAssetsForChain, pinnedFeeFields) Optional fund movement: all supported fungibles, ERC-20s first / native last with gas reserve, per-asset outcomes, NFTs excluded Recovery UI aborting the rest on one failure FE/test/recovery/legacyKeysMultiAsset.test.js legacyKeys.js:486,505,571

1.9 Backup registry (spec 032) and the deliberate exclusions

Seam Single source of truth for Gate Evidence
FE/lib/backup/syncedObjects.js#syncedObjects Exactly what rides the encrypted backup, each with networkScoped stated truthfully FE/test/backup/syncedObjects.test.js syncedObjects.js:47

Included (9): addressBook (net-scoped), preferences, vaultReferences (net-scoped), vaultCreationRecords, activityLedger (net-scoped), openChallengeCodes, legacyRecoveredKeys, miniAppState, hardwareAccounts. (evidence: syncedObjects.js:49,66,87,108,120,146,167,193,205)

Deliberately EXCLUDED — each with a test that reads syncedObjects.js as text and asserts absence:

Excluded store Key Why Asserting test
RPC endpoint overrides + credentials network_endpoints (in fw_global_prefs) device-scoped credential FE/test/applock/appLock.test.js:6 (comment), FE/test/network/endpointStore.test.js
Nav sections / density nav_sections, nav_density (fw_global_prefs) device preference FE/test/navPreferences.test.js:171 (reads the registry file)
GutterToken assistant key assistant_guttertoken_key_v1 device-only secret FE/test/assistant/guttertokenKeyStore.test.js:242
Member API key grants api_access_keys device-only capability material FE/test/apiKeys.test.js:220
Assistant memory assistant_memory_v1 device-local, member-clearable FE/test/assistantMemory.test.js:149
App-lock state fairwins.applock.state.v1 device session state FE/test/applock/appLock.test.js

1.10 Configuration seams

Seam Single source of truth for Callers Forbidden to bypass Gate / test Evidence
FE/config/contracts.js#getContractAddressForChain (+ getContractAddress, getDeploymentBlockForChain, DEPLOYED_CONTRACTS, DEPLOYMENT_BLOCKS) Contract address resolution per chain, sourced from deployments/ every contract read/write, mini-app host contracts() hardcoding an address; passing a Bitcoin string id FE/test/config/*, scripts/e2e/check-local-addresses.js (check:e2e-addresses) contracts.js:389,473,483,493,535
FE/config/contracts.js#isLocalOnlyChain Which chains a shipped build can never reach (1337) screeningChainIds(), roster filters treating a local chain as a degraded source FE/test/screening/screeningRoster.test.js contracts.js:560
FE/config/networks.js#membershipChainId Membership's ONE home per cohort — derived from MAINNET_CHAIN_ID/TESTNET_CHAIN_ID hasRoleOnChain, getUserTierOnChain, purchases a second literal 137 FE/test/config/*, assertReferenceChainInCohort() (:1185) networks.js:1097
FE/config/networks.js#miniAppChainId Mini-app registry home: Polygon 137 / Mordor 63; the one reference chain NOT derived from TESTNET_CHAIN_ID (no Amoy registry) registryClient hardcoding 137; deriving from TESTNET_CHAIN_ID FE/test/miniapps/registryClient.test.js networks.js:1156
FE/config/networks.js#cohortChainIds / isInCohort "All chains" = the build's cohort; constitution III boundary estate reads, screeningChainIds, listWrappableCoins listSupportedChainIds() for an estate read FE/test/chain/* networks.js:1165,1175
FE/config/bitcoinNetworks.js#isBitcoinNetworkId The EVM/non-EVM boundary guard for Bitcoin string ids every seam that takes a chain id passing a Bitcoin id into getContractAddressForChain/wagmi/subgraph FE/test/* bitcoin suites bitcoinNetworks.js:41,74
FE/config/passkeySupport.js#isPasskeySupported Where the passkey UserOp rail can submit (bundler present) resolveWriteRail, wrap, custody offering the passkey rail on ETC 61 / Mordor 63 FE/test/custody/writeRail.test.js passkeySupport.js:120
FE/config/perps.js#isEvmPerpVenue EVM-vs-non-EVM perp venue guard (Hyperliquid: string id, chainId: null) perps seams passing Hyperliquid into an EVM seam FE/test/perps/* perps.js:25,55
FE/config/tenant.js (tenantBrand, isFeatureEnabled, tenantThemeClass, tenantContractsForChain, tenantAssistantSettings, tenantChainIds) Tenant identity/settings/contract set, from tenants/<id>/manifest.json; selection is build-time VITE_TENANT_ID all shipped paths hardcoding a tenant identity value; runtime switching; falling back to another tenant on unknown id npm run tenants:validate (CI), FE/test/tenantBranding.test.js tenant.js:25,43,68,112,132,142,156,181
frontend/vite-plugins/tenant-branding.jsvirtual:tenant The build-time injection of the manifest into the bundle config/tenant.js, lib/miniapps/manifest.js a mini-app package resolving it (hard build failure — that is the intended guard) FE/test/miniapps/buildPreset.test.js, packageBoundary.test.js plugin file; consumers grep
FE/config/appNav.js (NAV_GROUPS, WAGERS_VIEW/WAGERS_PATH, TAB_ALIASES, visibleNavGroups, pathForNavItem, isNavItemEnabledForTenant) Nav structure + tab ids + tenant visibility drawer, sibling icon nav, search index, routes putting network back in NAV_GROUPS; a second nav structure FE/test/nav/navSearchIndex.test.jsx, AppNavDrawer.*.test.jsx appNav.js:24,80,94,97,143,224,248
FE/config/navSearchIndex.js (NAV_ITEM_TERMS, NAV_DESTINATIONS, accordionSectionForHash) Search synonyms + in-item destinations; descriptive, never authoritative AppNavDrawer search, assistant find_in_app letting an index entry resurrect a hidden surface; a second hash→section map FE/test/nav/navSearchIndex.test.jsx (asserts every navId is real) navSearchIndex.js:44,95,108,476,502
FE/lib/nav/navSearch.js#matchesQuery / rankEntries Matching: AND the terms, each a token prefix drawer search hand-written stems FE/test/AppNavDrawer.search.test.jsx navSearch.js:30,63,75,84
FE/lib/nav/attention.js (ATTENTION_PARAM='focus') Deep-link flash markers; absence degrades to plain navigation AttentionFocus (mounted once in App.jsx) a broken link on a missing marker FE/test/nav/attentionFocus.test.jsx attention.js:20-28,44,57
FE/components/admin/adminApps.js (ADMIN_APPS, buildAdminApps, adminAppForView, adminViewPath) The ONLY admin app→view→role matrix (9 apps: incident-response, compliance, membership-revenue, liquidity, protocol-config, maintenance, identity, access-control, infrastructure) Control Room tiles, per-app rails, AdminAppRoute guard, least-privilege tests a second app/view/gate mapping FE/test/admin/* adminApps.js:24,28,127,130,142,155,163
FE/config/wrappedNative.js#listWrappableCoins / getWrappedNative / hasWrappedNative Offered-beside-resolvable wrap candidates (cohortChainIds().filter(hasWrappedNative)) useWrapCoinOptions, useWrapNative a disabled row for an unconfigured chain; a guessed wrapper FE/test/* wrap suites wrappedNative.js:34,54,74
FE/hooks/useWrapNative (+ useWrapCoinOptions) Target-chain-bound wrap reads and per-rail retarget (classic switch-then-settle, passkey sendCalls {chainId}) Trade ▸ Wrap a MAX quoted on one chain riding to another (coin change clears the amount) FE/test/* wrap suites hooks listing
FE/config/newsAssets.js#newsSlugFor (chainId,address)→Alphaday tag slug; resolves client-side with no network call; null IS "not covered" lib/news/newsClient.js a ticker heuristic; a fallback slug FE/test/news/* newsAssets.js:1,35,44,62
FE/config/blockExplorer.js Explorer URL construction (defers to networks.js as source of truth) link-outs a second explorer table ⚠️ UNVERIFIED gate blockExplorer.js:11
FE/config/serviceCatalog.js Spec-060 serviceId catalog (fee services) fee disclosure surfaces hardcoding a bps value FE/test/custody/serviceCatalog.test.js file present
FE/lib/assets/assetActivity.js WHICH activity capabilities an asset has (spec 064) activity surfaces per-surface capability guesses ⚠️ UNVERIFIED header comment

1.11 Honest-reading seams (three-state / null discipline)

Seam Single source of truth for Forbidden Gate Evidence
FE/lib/format/amount.js#formatUnitsForDisplay (+ formatDecimalForDisplay) Base-unit → display string; null stays null, unparsable is null, never throws raw \|\| 0; rounding what is SENT FE/test/format/* amount.js:38,74 (if (raw == null) return null)
services/finops-exporter/src/reading.js (read / notConfigured / unreadable, redact, attempt) The three-state reading type — only one constructor takes a number a zero standing in for a failed read; collapsing not-configured into $0 npm run check:finops, npm run test:finops-gate (must-fail fixtures), services/finops-exporter/test/* reading.js:14,25,49,59,70,85
FE/hooks/usePerpsOrders.js Orders on the exit path; absent gateway key ≠ "member has none"; unreadableVenues named consulting screening/attestation/killswitch here FE/test/perps/safetyInvariants.test.js (asserted over this file's source) usePerpsOrders.js:1-30
FE/lib/perps/orderState.js What a perps order state MEANS (the state machine) re-deriving state in a hook FE/test/perps/* orderState.js:294
FE/hooks/usePerpsOrders/usePerpsTrade + lib/perps/venues/* THE ONE place a descriptor becomes venue calldata; PERPS_UI_FEE_RECEIVER the only FairWins address in it reaching for venue builders directly FE/test/perps/* usePerpsTrade header; perps.js:241; venues/gmx.js:291

1.12 Mini-app platform (spec 073)

Seam Single source of truth for Callers Forbidden Gate Evidence
FE/lib/miniapps/hostContext.jsx#MiniAppHostProvider / useMiniAppHost The entire privileged surface given to untrusted packages (hostApi 2): appId, wallet{address,chainId,isConnected,requestConnect,switchChain,submit}, readProvider, contracts, network, networks, store, audit, toast, navigate — wrappers, never handles mini-app iframe-less Blob-imported modules adding a key (grants it permanently to every package); handing out a signer/context/storage handle; reporting success from submit without SubmitResult.wait() FE/test/miniapps/hostContext.test.jsx; contract doc specs/073-miniapp-platform/contracts/host-context.md hostContext.jsx:13,172,408,416,585,772,938,980,1027
FE/lib/miniapps/hostScope.js#installHostScope (HOST_SCOPE_SYMBOL, HOST_API_VERSION, isHostApiSupported) How a package reaches the host object; hostApi compatibility loader a package importing host modules FE/test/miniapps/hostScope.test.js hostScope.js:57,67,126,155
FE/lib/miniapps/integrity.js (verifyManifestBytes, verifyFileBytes, keccak256Hex, sha256Hex) keccak(manifest bytes) vs chain + sha256 of every byte executed or injected loader.js trusting the SW cache (not a trust boundary) FE/test/miniapps/integrity.test.js, packageCache.test.js integrity.js:41,168,201
FE/lib/miniapps/loader.js#loadMiniApp / verifyMiniAppPackage Retrieval + verification + Blob-URL import; gateway list (resolveMiniAppGateways), size caps (MAX_MANIFEST_BYTES 64KB, MAX_FILE_BYTES 8MB) Apps section executing an unverified byte; verifyAllDeclaredFiles on a launch FE/test/miniapps/loader.test.js loader.js:80,87,94,240,547,684
FE/lib/miniapps/registryClient.js#normalizeApp Reads the chain's launchable — the serving decision — never status catalog, workspace re-deriving launchable; gating on status === Approved FE/test/miniapps/registryClient.test.js, registryAuthority.test.js dir
FE/test/miniapps/packageBoundary.test.js The import boundary in BOTH directions: nothing in frontend/miniapps/** may import frontend/src/**, and nothing in frontend/src/** may import a converted package tree either direction itself (a scoped vitest run cannot catch it — needs full suite or build) packageBoundary.test.js:1-40
FE/lib/miniapps/store.js (miniAppState, synced) + favorites.js (miniapp_favorites) Per-app namespaced store (root = appId); pinned apps (reserved admin-tool:<appId> namespace) host store, drawer pins cross-app store reads FE/test/miniapps/store.test.js, storeIsolation.test.jsx, favorites.test.js files
FE/lib/miniapps/hostRefusal.js (HOST_REFUSAL, MiniAppHostError) Normalized host refusal codes + member-facing messages host wrappers a raw throw reaching a package FE/test/miniapps/hostContext.test.jsx hostRefusal.js:13,57

1.13 Shared packages (spec 075 workspace)

Workspaces: frontend, frontend/miniapps/*, services/relay-gateway, services/finops-exporter, subgraph, tools/miniapp-build, packages/*. services/mcp-server is deliberately NOT a member (lockfile hazard) and has no dependencies. contracts/ is deliberately not a member.

Package Single source of truth for Consumers (verified) Gate
@fairwins/intent-types (packages/intent-types, exports . + ./offchain) EIP-712 intent structs + action metadata (INTENT_TYPES, INTENT_ACTIONS, CONTRACT_VERIFIED_TYPES, FUNDING_POOL_TYPES); off-chain ApiKeyGrant in ./offchain (deliberately outside CONTRACT_VERIFIED_TYPES) FE/lib/relay/intentTypes.js (re-export), FE/lib/apiAccess/apiKeys.js, FE/lib/transfer/eip3009Transfer.js, FE/lib/pools/gasless.js, FE/utils/claimCode/deriveFromCode.js, GW/intent/intentTypes.js (re-export), GW/x402/verify.js, GW/memberApi/{auth,intents,openapi}.js test/intent/TypehashParity.test.js (both directions, against contract typehashes), services/relay-gateway/test/actionCoverage.test.js, .../memberApiAuth.test.js (off-chain structs)
@fairwins/assistant-contract (exports ., ./prompt, ./tools, ./results) System prompt + tool definitions + honest result wording — ONE table FE/lib/assistant/{conversation,messageShape,providers/guttertoken,tools/executor,tools/toolLoop}.js; GW/memberApi/{assistant,openapi,routes}.js; vendored as services/mcp-server/src/toolDefs.snapshot.json services/relay-gateway/test/mcpToolParity.test.js (both directions), assistantContract.test.js, assistantTools.test.js
@fairwins/finops-catalogue (exports ., ./sources, ./schema) Every revenue + cost source declared once; label enumerations services/finops-exporter/src/server.js, scripts/finops/generate-dashboards.js npm run check:finops (C1–C5, incl. C2b payee discovery), npm run test:finops-gate
@fairwins/abi (exports ., ./json/*) Generated contract ABIs scripts/codegen/emit-abis.js (⚠️ no frontend/service import found — consumers may still read contracts/ artifacts) npm run check:abis

1.14 Relay-gateway internal module boundaries (GW/*)

Module Boundary it owns Forbidden Gate
GW/identity/ (middleware.js, routeTable.js, resolve.js, tiers.js, quotaKey.js, upstreamCeiling.js, verifiers/{attestation,challenge,grant}.js) spec 106 — undocumented in CLAUDE.md ROUTE_TABLE is THE ONE TABLE of protected routes; silence is a configuration error, not permission; a challenge is a metering upgrade, never an access gate (every READ is ANONYMOUS); writes need ADDRESS (an answerable party), never member a route absent from the table; putting a human minimum on a read; a second revocation store / membership reader services/relay-gateway/test/identity/ (asserts mounted set == declared set, both directions)
GW/access/ (routes.js, jwt.js, enforcement.js) spec 107 — undocumented Issuance of short-lived keyed-read credentials — issues, never carries traffic; every non-200 means "fall back to public capacity" making the gateway an RPC passthrough; gating issuance above ANONYMOUS services/relay-gateway/test/access.test.js
GW/intent/ (intentTypes.js re-export, verify.js, store.js) Signature recovery from the intent payload (self-authenticating); intent status store a local type table; trusting a caller-supplied signer actionCoverage.test.js, gateway.test.js, test/intent/TypehashParity.test.js
GW/paymaster/ (build.js, policy.js, sign.js) ERC-7677 sponsorship decision order: killswitch → per-op ceilings → sanctions → deposit gate → quotas → KMS sign; stub path never signs signing before screening; refusing on an unreadable deposit (RPC blip ≠ empty pool) paymaster.test.js
GW/polymarket/ (client.js, builderCode.js, normalize.js, routes.js) Builder-code attachment + fee caps (boot-fails above cap); read vs write quota split (write keyed by trader address) hiding the additive taker fee; pooling write budget into reads polymarket.test.js
GW/perps/ (client.js, normalize.js, routes.js) Per-venue failure isolation (read/degraded), null → "—", documented normalizer scale provenance; no write routes rendering a degraded venue as zeros; adding execution here perps.test.js
GW/bitcoin/ (client.js, cache.js, normalize.js, routes.js) Esplora reads + raw-tx broadcast + Stamps recognition; fail-safe stamp handling; 256kb body exception for the tx route spending an unverified-stamp-free UTXO; seeing key material (gateway sees addresses + signed raw tx only) bitcoin.test.js
GW/news/ (client.js, normalize.js, routes.js) TTL clamp ≥ 300 s, single-flight per slug, serve-stale ≤ 10×TTL then unreadable; honest-empty read with items: []; vendor image/icon/HTML never forwarded stale-as-live; forwarding vendor markup news.test.js
GW/memberApi/ (contract.js = ROUTES/SCOPES/ERROR_CODES, auth.js, routes.js, openapi.js, membership.js, wagers.js, intents.js, assistant.js, revocation.js) contract.js is the ONE route/scope/error table — routes.js mounts by contract id and boot-fails on a declared-but-unmounted route (routes.js:579-588); auth_unverifiable/membership_unreadable are retryable 503s, never denials; no write: scope exists typing a path in routes.js; a scope that authorises a write; an actor other than the token account memberApi.test.js (probes every ROUTES entry over HTTP), memberApiAuth.test.js, memberApiQuotaIsolation.test.js, memberApiSpend.test.js
GW/x402/ (paywall.js, requirements.js, verify.js, settle.js) Pay-per-request rail: bearer token checked first so a member never reaches the paywall; TransferWithAuthorization (not Receive…); token's own EIP-712 domain from chains.js#tokenDomain; verify-everything-before-settle; engine outage ⇒ 503 settlement_unavailable a free serve on engine outage; a new key/facilitator; a 1271 check for a contract payer (EOA-only, reason says so) x402.test.js
GW/fees/onchain.js The gateway's read-only FeeRouter view; env bps are fallback only a second fee-config store; a hardcoded bps fees.test.js
GW/policy/ (killswitch.js, quotas.js, sanctions.js, dedup.js, backpressure.js, reload.js) Global + per-module killswitch; quota windows (per-signer + global); fail-closed sanctions screen; SIGHUP live config reload (read via live getters, not captured booleans) screening after signing; a captured boolean that survives a reload rateLimit.test.js, gateway.test.js
GW/engine/client.js The one adapter to the OZ relayer engine (POST /api/v1/relayers/{id}/transactions); swappable a second submission rail; policy in the engine gateway.test.js
GW/audit/log.js, GW/metrics/counters.js Audit fields (never message content, never key material) and bounded metric labels unbounded labels (address/tx hash) memberApiSpend.test.js (counts only)
GW/config/index.js, GW/config/chains.js, GW/config/providers.js All env parsing + upstream base URLs + per-chain provider construction + tokenDomain; boot fails loudly on an out-of-cap fee or bad URL reading process.env in a module tenant.test.js, rpcEndpoints.test.js

3. DATA STORES & STATE

3.1 Browser — localStorage/sessionStorage via FE/utils/userStorage.js

Two namespaces: wallet-scoped fw_user_<lowercased address>_<key> (getUserStorageKey, userStorage.js:15-23) and device-global fw_global_prefs (userStorage.js:8).

Key Namespace Synced? Secret? Evidence
addressBook wallet synced (net-scoped) no addressBook/constants.js:10
preferences wallet synced no syncedObjects.js:66
custody_vault_references wallet synced (net-scoped) no vaultReferences.js:9
vault_creation_records wallet synced no vaultCreationRecords.js:16
activityLedger wallet synced (net-scoped) no syncedObjects.js:120
openChallengeCodes wallet synced opaque ciphertext syncedObjects.js:146
legacy_recovered_keys wallet synced ciphertext of private keys / BIP-39 (AES-GCM, PBKDF2-650k) — never cleartext legacyRecoveredKeysStore.js:11
miniAppState wallet synced no miniapps/store.js
hardware_accounts wallet synced no — public metadata only hardwareAccountsStore.js:11
network_endpoints fw_global_prefs deliberately UNSYNCED yes — RPC credentials endpointStore.js:21,36
assistant_guttertoken_key_v1 wallet deliberately UNSYNCED yessk-… key; redacted to sk-…+4 at every boundary guttertokenKeyStore.js:36
api_access_keys wallet deliberately UNSYNCED yes — member-signed grants apiKeys.js:40
assistant_memory_v1 wallet deliberately UNSYNCED text only, device-local memoryStore.js:22
assistant_prefs wallet not synced no assistantPrefs.js:34
nav_sections, nav_density fw_global_prefs deliberately UNSYNCED no navPreferences.js:24,26
miniapp_favorites (incl. reserved admin-tool:<appId>) fw_global_prefs not synced no favorites.js:29
app_lock (pref) / fairwins.applock.state.v1 pref / raw unsynced no appLock.js:27,30
fairwins.passkey.credentials.v1 raw localStorage unsynced credential ids/metadata, not key material credentials.js:26
fairwins.passkey.wrappedSeeds.v1 raw localStorage unsynced yes — PRF-wrapped master seed blobs prfKeys.js:24
fairwins.passkey.profile.v1, .explainer.v1 raw unsynced no accountProfile.js:6, explainer.js:10
fairwins.bitcoin.ledger.v1 raw unsynced no (address/cursor ledger) bitcoin/wallet.js:21
fw_account_profiles_v1 raw unsynced no accountProfilesStore.js:13
fairwins.transfers.v1 raw unsynced no transferStore.js:15
fairwins_notif_profiles_v1, fairwins_notif_delivery_v1 raw unsynced no notificationProfiles.js:25, deliveryPreferences.js:25
fairwins.perps.attestation, perps_pending_actions_v1 raw / feature unsynced no attestation.js:36, perpsActivityBuffer.js:45
fw_user_roles, fw_role_purchases raw unsynced no (cache of on-chain facts) roleStorage.js:14,15
tracked (ClearPath DAOs) wallet net-scoped no trackedDaos.js:24
friendMarkets, marketViewPreference, fairwins_home_v1, fairwins_landing_view_v1, fairwins.landing.stay.v1, fairwins.entryGate.ack.v1, pwaInstallPromptHidden, fw_pwa_install_snoozed (session), fairwins_wordlist_lang_v1, fairwins_qrcolor_v1, nullification_cache, wagmi.recentConnectorId mixed unsynced no see §grep in file list

3.2 Other client-side state

Store Name Contents Secret?
Service-worker cache fairwins-shell-v1 (CACHE_VERSION) app shell + OFFLINE_URLS no
Service-worker cache fairwins-miniapp-packages-v1 (MINIAPP_CACHE, LRU MINIAPP_CACHE_MAX_ENTRIES = 60) immutable CID-addressed package bytes; cache-first; not a trust boundary no
IndexedDB ⚠️ none found — no indexedDB/openDB use in frontend/src or public/
In-memory (module scope) assistant session token (assistantClient.js), screening caches (useAddressScreening map + screenEstate cache), readProvider per-provider cache, favorites revision, issued-access state ephemeral session token is sensitive, memory-only

3.3 Server-side / infra state

Store Contents Secret?
Gateway in-process intent store (GW/intent/store.js), revocation store (createRevocationStore, durable: false, maxEntries), quota windows (4 member-API instances + per-module), token budget (createTokenBudget), TTL caches per module, x402 replay protection (in-process — the token's own authorizationState is the durable guarantee), upstream ceilings, killswitch no secrets; all volatile
Gateway env vendor keys (OpenSea, Polymarket L2, Anthropic, engine API key), ORIGIN_AUTH_SECRET, WEBHOOK_SHARED_SECRET, KMS refs yes — delivered by infra/vm/common/fetch-secrets.sh
finops-exporter scheduler's last readings only no — refuses to boot if key material is in env
GCP Secret Manager scripts/secrets/registry.js declares every container + least-privilege profile: fairwins-deployer-key, fairwins-creator-key, fairwins-seed-player-keys, fairwins-floppy-{keystore,mordor,nazgul-prime}-password, fairwins-etherscan-api-key, fairwins-pinata-jwt, fairwins-graph-{deploy,api}-key, fairwins-quicknode-polygon-{token,url}, … Profiles: deploy, seed, verify, publish, rpc yes, all
Terraform state secret containers + access bindings only; never a google_secret_manager_secret_version (payload would land in state in plaintext). One exception: origin-lock header read via a data source — also lands in state, so the gate warns on each use to keep it countable contains no payloads by rule
deployments/*.json source of truth for on-chain addresses; 13 files (mainnet 1, optimism 10, etc 61, mordor 63, polygon 137, amoy 80002, base 8453, arbitrum 42161, hardhat/localhost 1337, admin-safe.json). Polygon keys include: wagerRegistry+Impl+wagerRegistryIntents, membershipManager+Impl, membershipVoucher, voucherBatchMinter, wagerPoolFactory+Impl+poolImpl, feeRouter+Impl, callsignRegistry+Impl, miniAppRegistry+Impl, bridgeRouter+Impl, liquidityRouter+Impl, safePolicyGuard, safePolicyGuardV2, policyGuardSetup, safeProposalHub, sanctionsGuard, accountFactory/accountImpl/keyRegistry/p256Verifier/entryPoint, verifyingPaymaster(+Signer), tokenFactory+Impl, open/restricted ERC20/721 Impl, backupPointerRegistry, chainlinkDataFeedAdapter, chainlinkFunctionsAdapter, umaAdapter, polymarketAdapter. ⚠️ fundingPoolFactory not present on Polygon public
Subgraph subgraph/schema.graphql entities indexed from chain (Graph-hosted Postgres) public
On-chain UUPS proxy storage (WagerRegistryCore is the single layout for both registry facets), membership tiers, pools, policy guard rules, vault approvedHashes, mini-app registry records, FeeRouter service rates, sanctions lists, EIP-3009 authorizationState public
tenants/<id>/manifest.json tenant identity/settings/contract set — never secrets (npm run tenants:validate) no
native-digests/, version.json build/release provenance no

4. TRUST BOUNDARIES

# Boundary What crosses Control that enforces it
B1 Browser → gateway (edge) every /v1/* call Cloudflare Transform Rule injects X-Origin-Auth; gateway timing-safe compares and answers 403 origin_denied; exempt only /healthz, /status, /v1/engine/webhook (server.js:248-261). CORS allow-list incl. native shell origins capacitor://localhost + https://localhost (config/index.js:442)
B2 Browser → gateway (caller identity) tier claim spec 106 GW/identity/middleware.js + ROUTE_TABLE: absence from the table is a configuration error, not permission; reads are ANONYMOUS (tier buys throughput), writes/signing need ADDRESS. Test asserts mounted==declared both directions
B3 Member → gateway (capability) member-signed fw1 grant GW/memberApi/auth.js verifies the off-chain EIP-712 ApiKeyGrant (one source: @fairwins/intent-types/offchain); scopes from contract.js; TTL-capped; revocation in-process and disclosed as non-durable; three verdicts — auth_unverifiable/membership_unreadable are retryable 503s, never denials
B4 Unauthenticated agent → gateway (payment) X-PAYMENT EIP-3009 authorization GW/x402/: bearer token checked first (a member never reaches the paywall); verify-everything-before-settle; sanctions fail-closed on the payer; served as the payer; EOA-only with a reason; 0 price = not offered, never free
B5 Gateway → vendor platform credentials, builder code, referral ids Credentials are gateway-only env from Secret Manager; per-module quotas + withUpstreamCeiling; killswitch; GW/config/index.js boot-fails on an out-of-cap fee; Hyperliquid builder hard-capped at 10 bps
B6 Gateway → engine (submission) tx calldata GW/engine/client.js is the only adapter; per-chain relayer id; one executor key per chain, never shared; all policy stays in the gateway
B7 Engine → gateway (callback) status webhook HMAC-SHA256 X-Signature over the exact raw bytes (req.rawBody), timing-safe, fail closed (no secret / no header / mismatch ⇒ nothing accepted)
B8 Host → mini-app package (untrusted third-party code) the host object only (a) on-chain curation + launchable (never status) + content-committed approveApp(id, expectedManifestHash) reverting StaleProposal; (b) integrity: keccak(manifest bytes) vs chain + sha256 of every executed/injected byte, re-checked after cache and network; © blob: in script-src only, never https:; (d) the host object is wrappers-only — sanctions screening happens inside wallet.submit before any rail; contracts(name) throws for an undeclared name; readProvider cached for stable identity; (e) FE/test/miniapps/packageBoundary.test.js enforces the import boundary both ways; (f) virtual:tenant + the preset's envPrefix make a bundled host config a hard build failure
B9 Member → vault (Safe multisig) proposals, approvals, executions SafePolicyGuard / SafePolicyGuardV2 are deliberately not upgradeable; migration is vault-consented setGuard; no matching rule ⇒ denial; approvers verified against the vault's own approvedHashes at nonce()-1 and only while still owners; client-side matchPreview is a twin that must move in lockstep; resolveWriteRail states an unavailable rail before the tap; signing a message is refused while operating as a vault
B10 Member's device → key material private keys, seeds, RPC/API credentials Legacy secrets AES-GCM under PBKDF2-650k, only ciphertext persisted; passkey seeds PRF-wrapped; hardware store holds public metadata only; every credential store is deliberately absent from syncedObjects.js with a test asserting it; redaction at every display/log boundary (redactRpcUrl, sk-…+4)
B11 CI → cloud Terraform plan/apply, image push IAM additive only (_iam_member; _binding/_policy rejected by npm run check:iac and the CI identity lacks projectIamAdmin — two layers); never a secret_version in state; adoption by import with prevent_destroy on unrecoverable resources; Terraform owns Cloud Run shape with a gate-enforced ignore_changes, Cloud Build owns the image; apply executes the reviewed plan, gated on the infra-tree digest; modules SHA-pinned from the private chippr-tf-modules
B12 Workstation → production funded deploy keys with live admin authority scripts/secrets/registry.js is the only inventory; npm run sec -- --profile <p> delivers a least-privilege bundle and nothing else; KEY/PASSWORD material never falls back to process.env on a public network; never written to disk/argv/log; operators impersonate a Terraform-declared SA (no key file), serviceAccountTokenCreator granted on the account, never the project; check:env-hygiene + npm run test:secrets gate drift; VITE_* reported as a NOTE (public by construction)
B13 Public internet → node interiors SSH no public :22 — open to the IAP range only; Ansible tunnels through IAP; widening the firewall is forbidden
B14 Cohort boundary (testnet ↔ mainnet) chain ids cohortChainIds() / isInCohort / assertReferenceChainInCohort(); constitution III forbids reads crossing it; miniAppChainId() deliberately does not derive from TESTNET_CHAIN_ID
B15 EVM ↔ non-EVM chain identifiers isBitcoinNetworkId (string ids), isEvmPerpVenue (Hyperliquid chainId: null), Solana derivation kept out of EVM seams
B16 Member ↔ GutterToken (BYOK) the member's own API key and messages Architectural: the browser calls api.guttertokens.com directly — FairWins never holds/forwards/sees the key or a message, charges nothing, and may not render a rate or credit figure. Legal amended in place (privacy §2/§5: GutterToken is not our processor)
B17 Assistant tool results ↔ app actions counterparty-authored text (prompt injection) No build_intent and no navigate in the in-app assistant; a tool result never makes the app DO anything; replyLinks.js is the only path from model text to a click; find_in_app is descriptive over the nav index; gateway refuses client-supplied tools


The on-chain half of this view — every contract, proxy, facet, role and chain — is Annex 07. Wire-level connectors are Annex 04.