Skip to content

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:

  1. The chain is the source of truth for money. Balances are read from token contracts, never from a cached number.
  2. The ledger cache is a cache, rebuildable from chain at any time, and rebuilding is a tested routine, not an emergency improvisation.
  3. 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.
  4. 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 400 from the export endpoint, kept distinct from the 409 used 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.