================================================================ AIPP — Ayoun Instant Payments Platform : demo on PostgreSQL ================================================================ [1] Setup: participants, aliases, held devices (seeded into PostgreSQL) ------------------------------------------------------------------------- ZANACO bank ZM Zambia National Commercial Bank MTN-ZM mno ZM MTN Mobile Money Zambia AIRTEL-MW mno MW Airtel Money Malawi ZM directory: jaweed@zanaco.zm, mary@zanaco.zm, chanda@mtn.zm, qr:shoprite-manda-hill, agent@mtn.zm MW directory: grace@airtel.mw at rest, the phone column holds ciphertext: b'gAAAAABq0qIMCBnsxv6d5-6C'... => ledger events: 19 head 6f3d5e816234e18b... [2] (a) Online alias payment ZM -> ZM: jaweed@zanaco.zm pays chanda@mtn.zm ZMW 250 ------------------------------------------------------------------------------------ status=completed state=COMPLETE txn=txn-ec35dcb762e7 latency=70.968 ms risk: score=15 action=allow reasons=['new_device'] retry with same idempotency key -> same txn (True), replay=True balance ZANACO-0001 9750.00 balance MTN-26096 3250.00 => ledger events: 31 head 69d93f06d84c2dce... [3] (b) Request-to-pay: Shoprite (QR) collects ZMW 180 from mary@zanaco.zm, Mary approves ------------------------------------------------------------------------------------------- collect col-f7429ab6df status=pending approved -> payment txn-dabf38f17d32 status=completed latency=53.075 ms; collect status=approved balance ZANACO-0002 2320.00 balance MTN-55501 680.00 => ledger events: 45 head 797b9e3016cd2c22... [4] (c) Cross-border ZM -> MW: jaweed@zanaco.zm pays grace@airtel.mw ZMW 1000 ------------------------------------------------------------------------------- federation_enabled=false -> status=failed: FederationDisabled: grace@airtel.mw is a foreign alias and federation is disabled (set AIPP_FEDERATION_ENABLED=true to exercise TCIB routing and FX) => ledger events: 48 head 734e08bde7ee3f51... [5] (d) Offline: Chanda loads ZMW 500 of tokens, goes offline, pays two devices --------------------------------------------------------------------------------- minted 6 signed tokens: [200, 120, 100, 50, 20, 10] (rows in `tokens`) balance MTN-26096 2750.00 balance OPERATOR:offline-suspense 500.00 --- network off: transfers are local signature checks only; no ledger events --- Chanda -> Shoprite POS 120: 1 token(s); Chanda -> Mary 80: 3 token(s); ledger events written: 0 --- network back on: Shoprite and Mary redeem --- Shoprite redeemed 120 (accepted 1, rejected 0) Mary redeemed 80 (accepted 3, rejected 0) balance MTN-26096 2750.00 balance MTN-55501 800.00 balance ZANACO-0002 2400.00 balance OPERATOR:offline-suspense 300.00 => ledger events: 63 head bfde78632acbe7ed... [6] (e) Double-spend: Chanda re-signs an already spent token to a second payee -------------------------------------------------------------------------------- Mary's phone accepted serial e40167c2... offline (signatures are valid) redeem result: credited=0 rejected=[{'serial': 'e40167c2-b38e-4c40-9690-ec1ea96c6b8f', 'reason': 'DoubleSpend: e40167c2-b38e-4c40-9690-ec1ea96c6b8f'}] DoubleSpendDetected events: 1; devices.status suspended: True; risk store (memory): suspended=True trust=0 => ledger events: 65 head 0c88b2fb8a0e18ff... [7] (f) Fraud: mule pattern — 8 payers fund one alias within a minute, then it fans out ----------------------------------------------------------------------------------------- payer0 -> mule 150: completed score=15 action=allow ['new_device'] payer1 -> mule 150: completed score=15 action=allow ['new_device'] payer2 -> mule 150: completed score=15 action=allow ['new_device'] payer3 -> mule 150: completed score=15 action=allow ['new_device'] payer4 -> mule 150: completed score=15 action=allow ['new_device'] payer5 -> mule 150: completed score=45 action=allow ['new_device', 'payee_fan_in:6_distinct_payers'] payer6 -> mule 150: completed score=45 action=allow ['new_device', 'payee_fan_in:7_distinct_payers'] payer7 -> mule 150: completed score=45 action=allow ['new_device', 'payee_fan_in:8_distinct_payers'] mule -> jaweed 1100: status=denied score=85 action=deny reasons: ['new_device', 'mule_fan_in_then_fan_out:8_payers_in_window'] compensation: none needed (denied before any funds were reserved) balance MTN-77777 1200.00 => ledger events: 185 head 09e9365a1dcef2e9... [8] (g) Settlement cycle: multilateral net positions -> settlement_cycles / settlement_positions -------------------------------------------------------------------------------------------------- MTN-ZM net 1250.00 OPERATOR net 300.00 ZANACO net -1550.00 sum of positions = 0 pacs.009 cyc-6be50b3f-001: AIPP-SETTLEMENT -> MTN-ZM 1250 ZMW on 2026-10-16 pacs.009 cyc-6be50b3f-002: AIPP-SETTLEMENT -> OPERATOR 300 ZMW on 2026-10-16 pacs.009 cyc-6be50b3f-003: ZANACO -> AIPP-SETTLEMENT 1550 ZMW on 2026-10-16 first instruction as ISO 20022 XML: => ledger events: 186 head f49eb119f3affe1d... [9] (h) Ledger integrity on PostgreSQL ---------------------------------------- checkpoint signed at seq 186, head f49eb119f3affe1d..., key f5235b6feb6d2818 ledger.verify() -> ok=True checked=186 checkpoints=1 attempting UPDATE ledger_events SET payload... WHERE seq=29: rejected by trigger: (psycopg.errors.RaiseException) ledger is append-only inserting a hand-built row with a wrong hash inside a transaction we roll back: verify() -> ok=False first_bad_seq=187 reason=hash mismatch (seq 187) after rollback: verify() -> ok=True => ledger events: 186 head f49eb119f3affe1d... [10] (i) Restart survival: dispose every connection, build a fresh platform, compare heads ------------------------------------------------------------------------------------------- head before restart: seq=186 hash=f49eb119f3affe1df9e0b9d36aaad1c8d6320e92fefafd64c28b99621799ebf7 head after restart: seq=186 hash=f49eb119f3affe1df9e0b9d36aaad1c8d6320e92fefafd64c28b99621799ebf7 same head: True; verify on new engine: True projections rebuilt from 186 events; balance ZANACO-0001 = 9750.00 Done.