Settlement & Data Model¶
The governing question for every field: does the kernel need this to enforce a rule? If yes, on-chain. If no, off-chain. On-chain data is public, permanent, and expensive, those three properties are the whole argument.
The split¶
| Data | Where | Why it must be on-chain |
|---|---|---|
| Account owner / validators | AuraAccount |
Determines who may sign |
| Payee address + trust state | AuraPayeeRegistry |
The hook reads it to pick a tier |
lastScreenedAt |
AuraPayeeRegistry |
Enforces screening freshness |
Coarse riskBand (0/1/2) |
AuraPayeeRegistry |
Enforces refusal of blocked payees |
| Policy: ceilings, limits, delay, co-signer | AuraSecurityManager |
The ladder itself |
| Velocity counters, queued intents | AuraSecurityManager |
Rolling limits; tamper-proof cooling-off |
| Session-key scope and expiry | AuraSessionKeyValidator |
Enforced, not advisory |
| KYC attestation (signed claim, no PII) | AuraComplianceRegistry |
Gates sending |
| Balances and transfers | Token contracts | It's money |
| Data | Store | Why not on-chain |
|---|---|---|
| Payee names, notes, avatars | Backend DB, encrypted | Privacy, a name on a public ledger is a leak |
| KYC documents, PII | KYC provider | Never ours to publish; GDPR |
| Full risk detail, sanctions hits | Guard service | Licensed provider data; also an oracle risk |
| Statements, categories, exports | Indexer, derived | Reconstructible; storing freezes it |
| AI conversations, device/session records | Backend | User/operational data, not consensus data |
Currencies¶
Four vetted stablecoins, and nothing else, an allowlist is itself a security control against malicious token contracts.
| Currency | Instrument | Decimals |
|---|---|---|
| US Dollars | USDC | 6 |
| Euros | EURC | 6 |
| Pounds | GBPT | 6 |
| (US Dollars) | USDT | 6 |
Privacy: names never touch the chain¶
The kernel must enforce "this payee is trusted" without the chain revealing who the payee is. The address is unavoidably public, it is the transfer destination. The name is not, and the link between them is what turns a payment graph into a social graph.
Rule: nothing that names, describes, or categorises a payee ever goes on-chain. The registry is keyed by raw address and holds only enforcement state.
For anything that must be committed but not revealed (later: payment references, invoice IDs),
the chain stores keccak256(salt ‖ value) and the preimage stays off-chain, integrity without
publishing content. Encrypted-at-rest columns (display_name, raw_result) use a per-user key,
so a database breach does not hand an attacker a social graph.
The GDPR payoff
Because names are never on-chain, an erasure request genuinely removes the personal data, what remains is pseudonymous transaction data. That is the strongest position available when GDPR meets an immutable ledger. (AML-required records are retained for the statutory period regardless, and the privacy notice says so.)
Reconciliation: the non-negotiable¶
Every figure is derived, not decorative. Operationally:
- The chain is the source of truth for money. Balances are read from token contracts, never from a cached number.
- The ledger cache is a cache, rebuildable from chain at any time, and rebuilding is a tested routine, not an emergency improvisation.
- Every statement reconciles:
opening + Σ movements = closing, checked against on-chain state at the closing block. A statement that does not reconcile is not rendered, it shows "this period is still reconciling" rather than a wrong number. - Reorgs are handled explicitly. A payment shown as cleared that later reorgs out is corrected and the user is told. Silently changing a balance is the worst possible behaviour for trust.
Money is bigints, end to end
Every monetary value is an integer in the token's base units, never a float, never
scaled through JavaScript's number. Amounts are validated as bigint at the boundary and
stay integers through to the token contract. Floating-point money is a class of bug that is
designed out, not tested for.
Statements over any date range¶
Statements support presets (3 months, 6 months, 1 year, All) and an arbitrary custom range, with CSV export carrying the same period.
The subtle part is mapping a date to a block. AURA binary-searches real block timestamps rather than dividing by an average block time:
Why not estimate blocks by average time
Chains skip blocks and halt. An estimate that lands early silently pulls in payments from
before the period; one that lands late silently drops payments inside it. Either way
the opening balance stops matching the transactions beneath it, and an unreconciled
statement is exactly what we refuse to render. The cost is roughly 26 extra
eth_getBlockByNumber calls per statement view; the fix if it ever bites is caching resolved
timestamps, not reverting to arithmetic.
Two deliberate behaviours:
- A period with no payments shows an empty statement stating the balance did not change, reconciled by the real reconciliation code, not asserted.
- A backwards or future date range returns
400from the export endpoint, kept distinct from the409used for a statement that will not reconcile. A typo must not send someone hunting a reconciliation problem that does not exist.
Verify by balance, not by receipt¶
A subtlety that has bitten this system before, and a rule for anyone extending it:
EntryPoint.handleOps reports success on a reverted operation
A UserOperation can revert inside a bundle whose outer transaction still succeeds. So a naive client reads the receipt, sees success, and reports "Sent" for a payment that never happened. State is confirmed by reading it back, a payee is "saved" only when the registry actually holds it; a payment "cleared" only when the balance moved. Never trust the receipt alone.