Skip to content
Aidan Alsaadoun

Settlement Engine

Event ingestion and on-chain/off-chain reconciliation for a Solidity prediction market.

Source on GitHubVerified on Sepolia GoStars: 2last push 2 weeks ago
  • Go
  • Solidity
  • PostgreSQL
  • Foundry
  • Docker Compose
  • JSON-RPC

What it is

A parimutuel market contract on Sepolia, plus a Go service that ingests the contract’s event logs into an append-only PostgreSQL ledger and continuously reconciles on-chain state against the database, alerting on any divergence.

Why it's interesting

Built to explore the correctness problems real-money trading platforms face: idempotent event ingestion, auditable state, and detecting drift between two systems of record.

Architecture

How an event becomes a verified row
  1. SettlementMarket.solSepolia · source of truth

    createMarket · buy · resolve · claim. Emits an event for every state change.

  2. Go indexerpolling loop

    Reads chunked block ranges up to latest − 6. The checkpoint advances only after every insert in a range succeeds.

  3. chain_eventsPostgreSQL · append-only ledger

    Idempotent insert: ON CONFLICT (tx_hash, log_index) DO NOTHING. Replaying any range is a no-op.

  4. foldderived, disposable

    Truncates and replays the ledger in chain order, in one transaction, into markets and positions.

  5. reconcilerreconciliation_runs · audit trail

    On-chain view calls vs off-chain SQL sums. Every comparison recorded; divergence raises an alert.

The contract’s pools and the database’s sums are two accounts of the same events, so they must always agree. The reconciler is the proof that they do.

What I built

  • Parimutuel yes/no market contract (createMarket, buy, resolve, claim) with Foundry lifecycle tests, deployed and verified on Sepolia. Checks-effects-interactions on claim, multiply-before-divide payout maths, call{value:} with the success flag checked.
  • A Go indexer where backfill and live tail are the same loop: each pass closes the gap between the last checkpoint and the safe head (latest − 6 confirmations), whether that gap is fifteen thousand blocks or five.
  • Idempotent ingestion keyed on (tx_hash, log_index) with ON CONFLICT DO NOTHING; the checkpoint advances only after every insert in a range succeeds, so a crash re-processes work instead of skipping it.
  • Derived state (markets, positions) is disposable by design: rebuilt from the ledger in chain order inside one transaction on every pass, so a corrupted projection heals itself on the next cycle.
  • A reconciler that compares on-chain pool totals (view calls) with off-chain SQL sums and records every check, pass or fail, in reconciliation_runs. A fault-injection demo corrupts a pool by hand and watches it get caught, then healed.
  • A transient-vs-fatal error taxonomy: RPC rate limits and timeouts retry the same range next pass; database and checkpoint failures stop the process, because a broken store with an advancing loop risks inconsistency.

Engineering concepts

  • Event sourcing
  • Idempotency
  • Checkpointing & recovery
  • At-least-once delivery
  • Reconciliation
  • Confirmation depth
  • Fault injection
  • Re-entrancy safety

Known limitations, on purpose

  • Centralised oracle: resolve is owner-only. A production market would use a decentralised oracle or dispute mechanism.
  • Confirmation depth rather than reorg rollback. Block hashes are stored in the ledger, so full rollback is a straightforward extension.
  • Polling rather than a WebSocket subscription; simpler and self-healing at this scale, and the backfill path doubles as crash recovery.
  • Full re-fold every pass rather than incremental. Deliberate at this event volume; at production volume the fold would advance from its own checkpoint.

How it was built

Hand-written, with AI used in three modes: tutor before writing, senior-style reviewer after, and a time-boxed unblocker for toolchain errors. What that workflow caught is documented in the repo’s NOTES.md.