Platform services: what the best platforms do, and what we build
2026-10-18 (PRODUCTION_PLAN Phase 3).
The node (arrekta-node, Rust; Python reference) owns consensus. Everything else is the
platform layer: indexing, issuer tooling, wallets and developer APIs. This note looks at
how established platforms build that layer and defines the four core platform services.
1. What established platforms do
| Need | Who does it well | Their approach | Licence / reuse |
|---|---|---|---|
| Explorer / indexer API | Blockstream Esplora (electrs fork); mempool.space; Trezor Blockbook | A separate indexer follows the node and serves a REST API: address history and UTXOs, block and tx lookup, fee estimates. Esplora's API is a de-facto standard that many wallets speak | Esplora front-end and backend: MIT (API design reusable). mempool.space and Blockbook: AGPL, so their code is never copied |
| Regulated asset issuance | Blockstream AMP (Liquid) | Transfer-restricted assets: holders are registered before they may receive. Assignments (an off-chain allocation) are separate from distributions (the on-chain transfer, signed by the issuer's own wallet). An optional issuer authorisation endpoint approves each transfer | Proprietary service; we reuse the model |
| Permissioned token rules | ERC-3643 / T-REX (Tokeny) | Every transfer is checked by an Identity Registry (holder eligibility, identity separate from wallet so keys can rotate) and Modular Compliance (transfer rules). Agents can freeze (full or partial), force-transfer, recover lost wallets and pause. Audits concentrate on those privileged roles | Open standard; we reuse the model |
| Wallet platform / custody | BitGo | Wallets with multisig or MPC. A policy engine: velocity limits, allowlists, initiator rules, and webhook policies where your server approves or denies a transaction | Proprietary; we reuse the model |
| Developer notifications | Alchemy Notify | Per-webhook signed HTTP POSTs (X-Alchemy-Signature), retries with exponential backoff. Consumers must dedupe and be reorg-aware | Proprietary; we reuse the model |
The common pattern is that the node stays minimal, and each platform capability is a separate service that follows the chain, keeps its own read model and exposes a focused API.
2. What we build: four services (Rust, sharing arrekta-core)
┌──────────── arrekta-gateway ────────────┐ API keys, quotas, OAuth,
clients ───────▶ │ REST /v1 · SSE/WebSocket · webhooks │ PQ-signed webhooks
└──┬──────────────┬──────────────┬──────┘
│ │ │
arrekta-indexer arrekta-issuer arrekta-wallet
(Esplora-compat (AMP + ERC-3643 (BitGo-style policies,
+ assets) model, enforced never holds keys,
by consensus) PST multi-party signing)
│ │ │
└──────── node events (P2P/SSE) ──── arrekta-node (consensus)
Postgres per service · chain events streamed from the node (SSE)
2.1 arrekta-indexer
- Follows the node over the node's event stream and REST API. On reorg it rolls back by height. It is a pure read model and can be rebuilt from genesis at any time.
- Esplora-compatible routes (
/block/:hash,/tx/:txid,/address/:addr/txs,/address/:addr/utxo,/blocks/tip/height,/fee-estimates, …) so existing explorer front-ends and wallet libraries can integrate with little change. - ARRK extensions:
/asset/:id(supply timeline: issued, minted, burned),/asset/:id/holders(cap table at any height),/asset/:id/frozen, per-address per-asset history. - Stronger than Esplora: cap tables at a historical height (needed for dividends and audits), and asset-aware balances.
2.2 arrekta-issuer (built: ISSUER_SERVICE.md)
The AMP issuer workflow plus ERC-3643's identity/compliance model, but the controls are enforced by Arrekta consensus (FREEZABLE/CLAWBACK, control and authority tokens), not by contract code that can be buggy.
- Investor registry: holders identified by an identity that owns one or more addresses, so a lost key means rotating the address, not re-verifying KYC (ERC-3643 idea). Claims (KYC, accreditation, jurisdiction) come from trusted claim issuers.
- Assignments → distributions (AMP idea): allocate off-chain, then distribute on-chain in batches signed by the issuer's wallet (arrekta-wallet produces PSTs, the issuer signs).
- Corporate actions: dividends and coupons from an indexer cap-table snapshot at a record height; burns and redemptions.
- Controls with governance: freeze, unfreeze and clawback require M-of-N approval inside the service, and the control token lives in an on-chain multisig lock. Every action carries a reason code and an audit record (the privileged-role risk auditors flag in ERC-3643).
- Transparency: hosts ARRK-AMS-1 metadata documents and reserve attestations, verifies them
against
metadata_hash, and publishes the controls policy that holders see before receiving.
What we improved after researching existing designs (implemented):
| Known weakness or proven pattern | Source | What arrekta-issuer does |
|---|---|---|
| Transfer restrictions in contract code are bypassable when one transfer path skips the check | Zealynx ERC-1400 review | Restricted assets live only in managed 2-of-2 addresses; the authorizer co-signature is the single path (clawback aside), with no "second transfer function" |
| AMP enforces restrictions with a co-signing server and an issuer authorization endpoint | Blockstream AMP | Same model on standard Arrekta multisig + PST; a per-asset authorizer key that can never move coins alone |
| Claims come from trusted claim issuers, scoped and expiring | ERC-3643 ONCHAINID, Polymesh | Claims must be ML-DSA-signed by a trusted claim issuer, carry an expiry, and can be revoked; registry operators can't invent them |
| Rules as OR of AND-groups with jurisdiction conditions | Polymesh asset compliance | Same rule shape, plus max holders, max balance and lock-up |
| A single privileged controller key is a backdoor; use multisig, time lock and a court-order hash | Secureum, Zealynx | Controls need an officer quorum; clawbacks wait out a time lock; every action carries a reason code and evidence hash; execution also needs the treasury wallet's co-signers |
| Assignments are separate from on-chain distributions | Blockstream AMP | Assignments (with vesting height) → batched treasury transfers settled by a worker |
| Snapshot dividends need a record date; rounding must be exact | SettleMint, Taurus CMTAT | Record height must be buried under N confirmations; largest-remainder allocation sums exactly; the table is hashed and signed |
| Reserve reports are point-in-time attestations | Circle USDC attestations | Attestations record the live circulating supply and chain height next to the report hash |
| Holders can't see how privileged roles were used | ERC-3643 audit checklist | A per-asset signed hash-chained public log of registrations, rule changes, executed controls, dividends, distributions and attestations |
2.3 arrekta-wallet (built: WALLET_SERVICE.md)
- Never holds private keys.
- Watch-only wallets: PQ schemes have no xpub, so clients register batches of locks or public keys (an address pool) generated on the client from their seed.
- PST, the partially-signed transaction format (Arrekta's analogue of Bitcoin PSBT): unsigned tx, spent outputs, required signers, then signatures merged from several parties. Supports multisig custody and hardware or air-gapped signers.
- Policy engine (BitGo model): per-wallet spend limits, velocity windows, address allowlists, and approval quorums. Webhook policies call the customer's server to approve or deny before co-signing.
What we improved after researching existing designs (implemented):
| Known weakness or proven pattern | Source | What arrekta-wallet / PST does |
|---|---|---|
| A PSBT change output can be swapped for a lock the attacker co-controls; multisig change is the weakest point | BitBox Trezor write-up, Coldcard, bitcoindev list | PST carries key origins per output. A signer counts an output as change only if an origin derives from its own seed to a key in redeem data that hashes to the lock (§16.3) |
| A coordinator can substitute co-signer keys in a multisig wallet | bitcoindev list; BIP 388 wallet-policy registration | Each co-signer's key batches are signed by its identity key. The service builds policies only from attested keys |
| PSBT v0 can't add inputs or outputs after creation | BIP 370 | PST modifiable flags with a constructor role (e.g. atomic asset-for-ARRK swaps), closed by the first signature |
| A tampered PSBT can exploit signer parsers or hide outputs | bitcoindev list (signing PSBTs proposed) | Every PST the service hands out carries an ML-DSA attestation; the decoder is canonical and fuzzed |
| Queued transactions re-pick the same inputs | BitGo unspent selection, reservations | Inputs of live transfers are reserved; manual holds for coin control |
| Approvals must come before signing; stale requests should lapse | Fireblocks TAP (two-tier, 2 h timeout) | pending_approval refuses signatures; transfers expire (default 2 h, 5 min–24 h) |
| Policy changes need an admin quorum; policies can be locked | Fireblocks admin quorum (7-day expiry), BitGo policy lock | Policy changes need the approver quorum within 7 days; lock makes the policy immutable until a date |
| New withdrawal addresses wait out a cooldown | Exchange allowlist practice (24–48 h) | Allowlist entries count only after allowlist_cooldown_secs (default 24 h) |
| Webhook policy approvals | BitGo webhook policy | Requests are ML-DSA-signed; anything but an explicit approve fails closed; public-only DNS resolver, no redirects |
Not adopted yet: Liana-style timelocked recovery paths need a relative-timelock lock kind, which is a consensus change and a candidate for a future soft fork.
2.4 arrekta-gateway
- API keys, scopes (
read,write,webhooks), per-key quotas and rate limits, idempotency keys on writes. - One public surface: proxies
/v1to node, indexer, issuer and wallet. Problem+json errors.ETagandIf-None-Matchpass through, so unchanged reads answer304 Not Modified.
Three ways to authenticate (implemented):
| Who | How | Details |
|---|---|---|
| Servers and scripts | API key: X-API-Key: arrk_… | Created by the admin (POST /gateway/v1/keys), shown once, stored as a SHA3 hash |
| Partners | API key plus a signature on every request | The key carries a post-quantum public key (signing_public_key on create, or PUT /gateway/v1/keys/{id}/signing-key). Each request sends X-ARRK-Timestamp, X-ARRK-Nonce and X-ARRK-Signature: v1,sig=<base64> over METHOD \n path?query \n timestamp \n nonce \n hex(SHA3-256(body)) \n key prefix (context ARRK-REQUEST-v1). Refused: timestamps more than 300 s off (stale-request), a nonce used before (replayed-request), a bad signature (bad-signature), and unsigned requests when the key has require_signature (signature-required). A leaked API key alone is then useless. The SDKs sign automatically (signingKey / signing_key) |
| End users of your app | OAuth 2.0 / OpenID Connect: Authorization: Bearer <JWT> | The gateway is a resource server for your identity provider (Auth0, Keycloak, Okta, Entra ID, Google…). It checks the token's signature against the provider's keys (JWKS, discovered and cached), the issuer, audience and expiry, and maps read / write / webhooks (or arrekta:…) scopes. The user becomes principal oidc:<sub> with its own rate limit, usage, webhooks and streams. Only asymmetric algorithms (RS*, PS*, ES256/384, EdDSA) are accepted. Settings: ARREKTA_GATEWAY_OIDC_ISSUER, _AUDIENCE, _JWKS_URL, _SCOPES_CLAIM, _DEFAULT_SCOPES, _RATE_PER_MIN |
The gateway does not run a login page or issue tokens itself: sign-in stays with the identity provider, which already handles passwords, MFA and account recovery.
- Webhooks done right:
- Events for address activity, asset issue/mint/burn/freeze/clawback, and tx confirmations at chosen depths.
- Reorg-safe: explicit
confirmedandreorged(retraction) events; every delivery carries anevent_idfor dedupe; retries with backoff; auto-suspend after repeated failures. - PQ-signed deliveries: each payload is signed with the platform's ML-DSA key, and the public key is published, so receivers can verify authenticity even against a future quantum attacker. Alchemy and BitGo use classical HMAC/ECDSA signatures.
- Streaming: SSE and WebSocket subscriptions with resume-from-
event_id(built: STREAMS.md).
Streams: what we adopted after research (implemented):
| Pattern or pitfall | Source | What the gateway does |
|---|---|---|
EventSource resends Last-Event-ID; servers must define replay semantics and retention | WHATWG SSE (via SSE test recipes), Ably | Ids are store seq; resume is strictly-after; history kept 7 days; older cursors get an explicit stream.reset with a REST resync hint, never a partial story |
| Monotonic ids, dedupe, at-least-once | Ably / Svix guides | Inserts serialised so seq commits in order (no skipped events on resume); event ids are stable for dedupe |
Reorgs: resend removed logs flagged removed | Ethereum eth_subscribe (Infura docs) | event.retracted for every event of an orphaned block, carrying the original's addresses/assets so filtered streams get it |
| Bounded per-client queues; transaction feeds must replay, not drop | WebSocket gateway design guides | Pull-from-store by cursor: slow consumers fall behind with bounded memory and replay on reconnect |
| Browsers can't set WebSocket/EventSource headers; tokens in URLs leak to logs | WebSocket auth guidance | Short-lived, stream-only st_ tokens (revoked with the key) instead of API keys in URLs |
| Heartbeats and half-open detection | WebSocket API design guides | SSE keepalive comments every 15 s; WS pings every 20 s, close after 60 s silence; app-level ping/pong |
| Address tracking subscriptions (batched, with limits) | mempool.space WebSocket API | Address/asset/subject filters per subscription; 10 subscriptions per socket, 20 streams per key |
| Events emitted only for committed changes | Transactional outbox pattern | Wallet/issuer audit rows (same transaction as the change) relayed to the gateway; gap-aware cursor |
3. Why this is stronger and more distinctive
- Post-quantum end to end: keys, transactions, P2P transport and webhook signatures.
- Compliance enforced by consensus, not smart contracts: no contract-upgrade or re-entrancy class of bugs, and holders can see an asset's controls before receiving it.
- Ecosystem compatibility: Esplora-style API for explorers and wallets, PST for multi-party signing.
- Reorg-aware by design: every read model and webhook handles retractions explicitly.
- No custody risk by default: the wallet service holds no keys; issuers sign with their own wallets.
4. Build order
| Step | Deliverable |
|---|---|
| 1 ✅ | arrekta-indexer + Esplora-compatible API + asset extensions, reorg rollback, rebuild-from-genesis |
| 2 ✅ | arrekta-gateway: keys, quotas, proxy, PQ-signed reorg-safe webhooks |
| 3 ✅ | arrekta-wallet: address pools, PST format (spec + vectors in PROTOCOL.md), policy engine |
| 4 ✅ | arrekta-issuer: registry, assignments/distributions, corporate actions, governed controls |
Each step ships with tests, a schema job (a one-shot job owns the database schema), a compose
service and an end-to-end check in all_svc/.
Sources
- SSE test recipes (Last-Event-ID semantics) · Resume tokens and Last-Event-ID (Ably) · Restoring WebSocket subscriptions (OneUptime)
- eth_subscribe reorg semantics (Infura) · alchemy_minedTransactions
- Stripe: processing undelivered events
- WebSocket API design and governance (Gravitee)
- mempool.space WebSocket API
- ERC-3643 audit checklist (QuillAudits) and ERC-3643 guide (QuickNode)
- Blockstream AMP API specification and transfer-restricted assets
- Polymesh compliance
- ERC-1400 compliance pitfalls (Zealynx) and Secureum security pitfalls 201
- Cash dividends (SettleMint), CMTAT dividends (Taurus), corporate actions (Allium)
- USDC monthly attestation detail (Circle)
- BIP 174: PSBT and BIP 370: PSBT v2
- BIP 388: Wallet policies
- A theft attack on Trezor Model T (change swap)
- Coldcard: verifying transactions
- bitcoindev: PSBT signing and multisig change
- Fireblocks approval and signing expiration
- Fireblocks governance and policy engine
- BitGo unspent reservations
- BitGo unspent selection improvements
- BitGo add wallet-policy rule (rule types, lock)
- BitMEX address-book whitelists (cooldown)
- Liana wallet with BitBox02 (timelocked recovery)
- Blockstream AMP overview
- AMP: Transfer-restricted assets
- AMP: Shares, dividends & rewards
- AMP API tutorial (Python)
- ERC-3643 explained (Spark)
- ERC-3643 guide (QuickNode)
- ERC-3643 audit checklist (QuillAudits)
- Esplora (GitHub)
- Esplora announcement
- Bitcoin explorer API comparison
- BitGo policies overview
- BitGo webhook policy rules
- BitGo webhooks
- Alchemy webhooks overview
- Alchemy webhooks best practices (Hookdeck)