Sabi Market
A Nigeria-first prediction market with integer-price CLOB, conditional-token settlement, and optimistic-oracle resolution
Version 1.0 — April 2026
Authors: Sabi Market Engineering
Status: Production Blueprint
Abstract
Sabi Market is a naira-denominated event-contract exchange where participants trade binary YES/NO shares on the outcomes of real-world events. Prices reflect the market's collective estimate of the probability that each outcome occurs. Under efficient-market conditions, the traded price of a YES share equals the market-implied probability that the underlying event resolves YES.
This paper specifies the complete algorithm that powers the exchange: how orders are matched, how shares are created and destroyed, how escrow maintains full collateralization at all times, how markets resolve with economic guarantees against bad resolutions, and how the ledger preserves a balance-to-zero invariant that any auditor can verify.
The design draws from two established exchanges — Polymarket (content-addressable market identity, optimistic oracle, hybrid CLOB) and Kalshi (regulated custodial model, event-contract framing, centralized clearinghouse) — and adapts them to the Nigerian market context: single-currency (NGN) denominated contracts, integer-kobo prices, progressive KYC gated on first deposit rather than at signup, and payment-provider-native deposits (Paystack, Monnify) rather than stablecoin rails.
1. Introduction
1.1 What Sabi Market does
A user asks "Will Super Eagles qualify for AFCON 2027?" The market opens with a YES price around 72 kobo — meaning the book collectively estimates a 72% probability of qualification. A user who thinks this is too optimistic can buy NO shares at 28 kobo each; if Nigeria fails to qualify, each NO share pays ₦1 and the user earns a 257% return on capital. If the event resolves YES, each YES share pays ₦1, NO shares expire worthless, and the payouts settle instantly from a fully-collateralized escrow account.
1.2 What Sabi Market is not
Sabi Market is an event-contract exchange, not a sportsbook, casino, or lottery. The distinction is legal, operational, and philosophical:
- A sportsbook is the counterparty to every bet; Sabi Market never takes a position
- A sportsbook sets odds; Sabi Market's prices emerge from user activity
- A sportsbook profits from an expected-value edge against users; Sabi Market charges a small trading fee and is outcome-indifferent
- Sportsbooks are regulated under gaming law; event contracts are regulated under commodities/securities law (SEC Nigeria in our jurisdiction; CFTC for Kalshi; pre-regulation for Polymarket)
1.3 Why prediction markets matter
Prediction markets aggregate information into prices more efficiently than polls or expert opinion. Famously:
- The Iowa Electronic Markets outperformed polls in predicting U.S. presidential election outcomes over five election cycles
- Kalshi's 2024 election markets tracked the final vote share more closely than major pollsters
- Polymarket's markets on central-bank rate decisions correlate with FOMC outcomes at 0.85+
For Nigeria specifically, prediction markets offer:
- Information infrastructure in an environment where poll data is sparse, expensive, and often unreliable
- Hedging for SMEs exposed to FX, rate, and election-outcome risk
- Civic engagement via quantified forecasts of CBN policy, INEC outcomes, and NGX performance
2. The pricing model
2.1 Share mechanics
Each market M has two share types: YES shares that pay ₦1 if M resolves YES and ₦0 otherwise, and NO shares that pay ₦1 if M resolves NO and ₦0 otherwise. The market satisfies the complementarity identity:
For any holder h of n_YES YES shares and n_NO NO shares in market M:
max_payout(h) = max(n_YES, n_NO) × ₦1.00A critically important sub-case: if n_YES = n_NO, the holder is guaranteed exactly n × ₦1.00 regardless of outcome — because exactly one of YES or NO pays ₦1 and the other pays ₦0. This property is what allows us to mint new shares from thin air without taking on exchange risk, as we will see in §4.
2.2 Prices as probabilities
Prices are integers in the range [1, 99] kobo per share. This integer constraint eliminates an entire class of rounding and comparison bugs that plague floating-point financial systems. A fractional probability of 0.72 is represented exactly as the price 72 (meaning "72 kobo buys one share that pays 100 kobo if YES").
The no-arbitrage condition requires:
P_YES + P_NO = 100 koboIf this condition is violated — say YES is trading at 40 and NO at 30 — an arbitrageur can buy one of each, pay 70 kobo total, and receive a guaranteed 100 kobo payout, earning a risk-free 42.9% return. The matching engine's structure makes this condition an enforceable identity: if best-YES-bid + best-NO-bid > 100, a pair can be minted, removing the arbitrage; if best-YES-ask + best-NO-ask < 100, a pair can be burned, likewise.
Under efficient trading, the YES price is the market-implied probability of YES resolution, expressed as an integer percentage.
2.3 Why integer kobo
Three reasons:
- Exactness.
0.72 × 100 = 72in integer arithmetic but not always in IEEE 754 floats. Every production fintech that stores money as floats eventually has acents_owed != cents_paidbug. We sidestep this entirely. - Cardinality. 99 possible prices (1 through 99) is ample for a retail forecasting exchange — the granularity exceeds human probability-estimation precision. Power users who want sub-1% resolution are typically wrong to want it; the market noise is bigger than the grid.
- Auditability. Every balance, fill, and fee in the system is expressible as an integer. Running-total sanity checks (e.g., "does the ledger balance to zero?") become exact comparisons, not epsilon-bounded.
3. Matching engine architecture
3.1 Central Limit Order Book (CLOB)
Sabi Market runs a classical price-time-priority CLOB per market. The per-market state consists of four ordered queues:
| Book | Contents | Sort order |
|---|---|---|
buy_yes | Limit bids to buy YES shares | Price DESC, time ASC |
sell_yes | Limit asks to sell owned YES shares | Price ASC, time ASC |
buy_no | Limit bids to buy NO shares | Price DESC, time ASC |
sell_no | Limit asks to sell owned NO shares | Price ASC, time ASC |
When a new order arrives, the engine tests it against both its same-side counter-book (e.g., a new buy_yes tests against resting sell_yes) and its cross-side pair-minting/burning candidate (a new buy_yes also tests against resting buy_no). It executes the path with the most favorable price for the taker, iterating until either the order is fully filled or no profitable fill remains. Residual quantity rests on the book.
3.2 Why CLOB and not AMM
Polymarket originally used an automated market maker (LMSR); it migrated to CLOB in 2022 because the AMM consistently bled liquidity-provider capital during informed-trader events (e.g., Trump debate performances). Kalshi was CLOB from day one.
For the Nigerian context, CLOB is correct because:
- Liquidity is thin and bursty — concentrated around match days, election nights, CBN MPC announcements. An AMM with fixed
bliquidity parameter is either over-capitalized most of the time (wasting LP capital) or under-capitalized during spikes (suffering adverse selection). - We have professional market-maker candidates in Lagos (quant desks, crypto trading firms) who prefer CLOB interfaces over LP positions.
- CLOB reveals book depth, which is the single most important quality signal for a new exchange. An AMM hides fragility behind a smooth price curve.
Sabi Market reserves the option to enable a Logarithmic Market Scoring Rule (LMSR) backstop for cold markets — an automated maker that quotes both sides when the CLOB has no resting orders. This is implemented as a first-class participant on the book, indistinguishable from any other maker to the engine. It is off by default and activated per-market when the operator determines that the market would otherwise be too illiquid to open.
3.3 Single-writer per market
Each market is its own deterministic state machine. Order submissions are serialized per-market via a database transaction (SQLite BEGIN IMMEDIATE) so that no two orders in the same market can race. Cross-market operations (e.g., wallet deductions) are composable because the ledger itself is globally atomic.
This is the same design pattern used by LMAX Exchange and Polymarket's CLOB: single-writer per market, no distributed consensus needed, deterministic replay from an event log. At Nigerian volume profiles (<10k orders/sec per market for the foreseeable future) a single SQLite writer suffices; under growth we migrate to PostgreSQL row-locked writes and, at extreme scale, to a dedicated Rust actor-per-market system with the Postgres ledger as the source of truth.
4. The three execution paths
This is the heart of the algorithm. Any fill on Sabi Market executes via exactly one of three paths.
Let us define:
P= the taker's limit priceM= the best-matching maker order at priceQs= the fill size (shares)- Escrow = the market-specific custodial pool that holds collateral for outstanding shares
4.1 Path A — share_transfer
A buy_yes taker at price P meets a sell_yes maker at price Q ≤ P, where the maker is an existing holder of YES shares wishing to exit.
Mechanics:
sYES shares transfer from maker's position to taker's position- Taker paid
P × sto escrow on order entry; maker is now paidQ × sfrom escrow; taker is refunded the excess(P - Q) × sfrom escrow back to their wallet
Ledger entries (single balancing transaction):
escrow -P × s (release taker's full reserve)
taker.wallet +(P - Q) × s (refund excess)
maker.wallet +Q × s (maker's proceeds)
Sum: -Ps + (P-Q)s + Qs = 0 ✓Net: escrow is net unchanged; shares move between users; wallet balances adjust. The trade price shown on the tape is Q (the maker's price) — this is standard price-time-priority convention: the taker gets price improvement, the maker gets their quoted price.
The symmetric case (sell_yes taker crossing buy_yes maker, or either NO-side variant) is identical under relabeling.
4.2 Path B — pair_mint
A buy_yes taker at price P_y meets a buy_no maker at price P_n, where P_y + P_n ≥ 100 (i.e., the combined bids cover the cost of creating one complementary pair).
Mechanics:
- A new YES share and a new NO share are minted simultaneously. The YES goes to the taker, the NO goes to the maker.
- Taker's effective cost per share is
100 - P_n(they pay whatever's left after the maker's contribution). - Escrow gains exactly
100 × skobo — the ₦1 face value ofsnewly-minted pairs. - Taker's excess — they limited at
P_ybut effectively paid100 - P_n, soP_y - (100 - P_n) = P_y + P_n - 100is refunded.
Ledger entries:
Before: taker has reserved P_y × s on order entry; maker has reserved P_n × s. Escrow thus holds (P_y + P_n) × s in reserves. Target: escrow retains 100 × s (the minted pair's face value). The rest returns to the taker.
escrow -(P_y + P_n - 100) × s (excess leaves escrow)
taker.wallet +(P_y + P_n - 100) × s (refund to taker)
Sum: 0 ✓
Escrow net change = +100 × s (from original reserves) - (P_y + P_n - 100) × s (refund)
= (P_y + P_n) × s - (P_y + P_n - 100) × s
= 100 × s ✓Open interest (OI) for the market increases by s — the count of outstanding YES/NO pairs.
Why this is safe. Escrow now holds exactly 100 × s additional kobo against exactly s new YES shares and s new NO shares outstanding. If the market resolves YES, we pay 100 × s to the YES holder (the taker). If NO, we pay 100 × s to the NO holder (the maker). Either way, escrow discharges exactly the liability it took on. No exchange risk.
4.3 Path C — pair_burn
A sell_yes taker at price P_y meets a sell_no maker at price P_n, where P_y + P_n ≤ 100. Both are existing holders of their respective shares; both want to exit.
Mechanics:
- Both sides' shares are burned (removed from circulation). OI decreases by
s. - Taker receives
100 - P_nper share (their price improvement); maker receivesP_nper share. - Escrow releases exactly
100 × s— the liability against the now-destroyed pairs is extinguished.
Ledger entries:
escrow -100 × s
taker.wallet +(100 - P_n) × s
maker.wallet +P_n × s
Sum: -100s + (100 - P_n)s + P_n s = 0 ✓The YES seller's s YES shares and the NO seller's s NO shares are destroyed, OI decreases by s, and wallet balances reflect the agreed-upon split of the ₦1-per-pair escrow collateral.
4.4 Worked example
Market: "Will Super Eagles win their next match?" — currently trading around 58% YES.
t=0: House MM places resting orders. buy_yes @ 57, size=200, buy_no @ 41, size=200, buy_yes @ 56, buy_no @ 40, etc. The book now shows 2-kobo spread around 58.
t=1: Trader Alice believes YES is underpriced and places buy_yes @ 60, size=50. The engine evaluates:
- Same-side (
sell_yes) candidate: none (no resting sell_yes). - Cross-side (
buy_no) candidate: bestbuy_nois41. Cross condition:60 + 41 = 101 ≥ 100✓. Eligible.
The engine executes Path B (pair_mint) against buy_no @ 41:
s = min(50, 200) = 50- Taker's effective cost per share =
100 - 41 = 59 kobo - Escrow receives: Alice's reserve
60 × 50 = 3,000 kobo+ MM's reserve41 × 50 = 2,050 kobo=5,050 kobo - Post-mint escrow retains
100 × 50 = 5,000 kobo; refunds Alice the50 koboexcess. - Alice's wallet: debited
3,000, refunded50, net-2,950 kobo=-₦29.50 - Alice's position:
+50 YES shares @ effective price 59 kobo - MM's position:
+50 NO shares @ price 41 kobo - Market OI:
+50 pairs, volume:+₦50, last trade price (YES):59 kobo
t=2: Super Eagles win 2–1. Oracle proposes YES with a ₦5,000 bond; 24 hours pass without challenge; market resolves YES.
- Alice's 50 YES shares each pay ₦1 → she receives
50 × 100 = 5,000 kobo=₦50 - Alice's realized P&L: received
₦50.00, spent₦29.50, profit₦20.50(+69%) - MM's 50 NO shares pay ₦0 (worthless)
- MM's realized P&L:
-₦20.50(they were wrong about the probability) - Escrow released:
50 × ₦1 = ₦50to Alice; MM's ₦20.50 stays destroyed (transferred to Alice via the escrow mechanic) - Post-resolution escrow balance for this pair: exactly 0 ✓
This is the algorithm in action. Every fill is one of these three paths; every path preserves the solvency invariant.
5. The solvency invariant
5.1 The core identity
For every market M at every point in time t:
escrow_balance(M, t) = open_interest(M, t) × 100 kobo
+ Σ_{o ∈ open_buys(M, t)} (o.price_kobo × (o.size - o.filled))In words: the market's escrow holds exactly the amount needed to pay every outstanding share at face value, plus the reserves from every open buy order whose capital is currently locked pending fill or cancellation.
5.2 Proof by structural induction
We prove this by showing each of the four state-changing operations preserves the invariant:
1. Order submission (buy side): A new buy_yes or buy_no at price P size s debits P × s from the user's wallet and credits P × s to escrow. LHS increases by P × s; RHS increases by P × s (the new open-order reserve term). Invariant holds.
2. Order cancellation (buy side): Refund P × (s - filled) from escrow to wallet. LHS decreases by that amount; RHS also decreases by the removal of that open-buy reserve. Invariant holds.
3. Path A (share_transfer): Shown in §4.1 — escrow unchanged, OI unchanged, open-buy reserves decrease by P × s (the taker's reserve is consumed). Taker's reserve was P × s, taker now pays Q × s to the maker and is refunded (P - Q) × s. Total flows: -P × s out of escrow as reserves; invariant holds because taker's reserve term in RHS decreases by the same P × s.
4. Path B (pair_mint): §4.2 — escrow increases by 100 × s (after refund), OI increases by s. LHS grows by 100 × s. RHS: OI × 100 term grows by 100 × s; reserve term decreases by (P_y + P_n) × s (both orders consumed) and increases by nothing new. But wait — we need the reserve-term decrease to equal the excess-refund. (P_y + P_n) × s - (P_y + P_n - 100) × s = 100 × s. So net RHS change: +100s - (P_y + P_n)s + (P_y + P_n - 100)s — let's compute: +100s - (P_y+P_n)s + (P_y+P_n)s - 100s = 0. Hmm. Let me restate cleanly.
Before pair_mint: LHS = E, RHS = OI × 100 + R where R includes the two orders about to match. After: LHS = E + 100s (escrow gained the pair face value net of refund). OI grows by s, so OI × 100 grows by 100s. Open-buy reserves decrease by (P_y + P_n) × s (both orders consumed) — but the refund of (P_y + P_n - 100) × s to taker is part of that same decrease on the escrow side. Both sides increase by 100s. Invariant holds.
5. Path C (pair_burn): §4.3 — escrow decreases by 100 × s, OI decreases by s. OI × 100 term decreases by 100 × s. No change to reserves (sell orders don't reserve kobo). LHS and RHS both decrease by 100s. Invariant holds.
6. Resolution (settle): Market outcome o ∈ {YES, NO}. Every holder receives shares × ₦1 if they hold the winning side. Total paid out = OI × 100. Escrow decreases by OI × 100. OI then resets to 0. LHS loses OI × 100; RHS loses OI × 100. Open-buy reserves refunded to all holders of cancelled-on-close orders; that term zeros out on both sides. Post-resolution: LHS = 0, RHS = 0. Invariant holds.
By induction over the sequence of operations, the invariant holds at every state of every market for all time. We cannot become insolvent through matching-engine operations alone.
5.3 What can break solvency
The invariant above concerns matching-engine operations. Real-world solvency risk comes from:
- Payment provider failures — if Paystack/Monnify webhook-confirms a deposit that later reverses, escrow may hold credited balance against no real NGN in the trust account. Mitigation: 48-hour card-deposit holds, webhook signature verification, end-of-day reconciliation against trust account.
- Oracle errors — if a market resolves incorrectly, the settlement transfers funds from losers to incorrect winners. Mitigation: optimistic oracle with bonds (§6), dispute window, void-and-refund option.
- Operational error — a developer writes a raw
UPDATE users SET wallet_balance = …bypassing the ledger. Mitigation: all money movements flow throughledger.post()which enforces balance-to-zero; hourlyauditLedger()cron that flags any non-zero txn_id sum.
The code-level invariant is strong. The operational invariants require discipline and monitoring; see §10 on risk controls.
6. Market resolution
6.1 The oracle problem
Every prediction market must answer: how do we determine the true outcome of an event? Three broad approaches exist:
| Model | Used by | Tradeoff |
|---|---|---|
| Centralized admin | Kalshi, PredictIt | Fast, trusted if you trust the operator; zero economic guarantee against operator error |
| Committee multisig | Augur v1, early Polymarket | Requires external reputation; slow; coordination overhead |
| Optimistic oracle | UMA, Polymarket, Sabi Market | Economic guarantee via bonds; fast in the common (unchallenged) case; escalation path for disputes |
Sabi Market uses optimistic oracle with bonds, adapted from UMA's design and Polymarket's production-tested implementation.
6.2 The flow
- Trading closes at
close_at(pre-specified at market creation). - Proposal window opens. Any user can propose an outcome (YES, NO, or VOID) by calling
proposeResolution(market_id, outcome, evidence_url, bond). The proposer stakes a minimum bond of ₦5,000, which escrows in a per-proposalbond_pool. - Challenge window. For 24 hours after the proposal, any other user can challenge the proposal by calling
challengeResolution(proposal_id, counter_outcome, evidence_url, bond). The challenger stakes an equal bond. - Unchallenged path (common case). If no challenge arrives within 24 hours, the proposal auto-finalizes. The proposer's bond is refunded. The market settles per §5.2 step 6 using the proposed outcome.
- Challenged path. A challenge halts automatic finalization and escalates to the Resolution Committee — a 5-of-7 multisig of domain experts (INEC officials, sports federation staff, CBN-watchers, NGX-watchers). The committee reviews evidence and declares a final outcome. The winner (proposer or challenger) receives both bonds as compensation for the correctness and the review cost; the loser forfeits their bond.
6.3 Economic properties
The bond structure creates:
- Skin in the game. A frivolous or malicious proposal costs ₦5,000 minimum. Over any reasonable volume of attacks, the attacker loses money.
- Challenge incentive. A correct challenger earns both bonds — doubling their capital. This attracts rational actors to challenge incorrect proposals.
- Truth-telling equilibrium. At equilibrium, proposers only submit correct outcomes (incorrect proposals will be challenged and lose the bond). Challengers only challenge when they are confident the proposer is wrong. The common case is therefore unchallenged truth, fast resolution, and no committee burden.
The system's security parameter is the bond size. For markets with higher open interest, the minimum bond scales up: for a market with ₦50M open interest, the bond should be large enough that manipulating the resolution would cost more than the largest position manipulation could profit. A rough rule:
min_bond = max(₦5,000, 0.5% × open_interest)This is enforced at proposal submission time.
6.4 Void conditions
A market may resolve VOID when:
- The underlying event does not occur in a form that can be unambiguously classified (e.g., a match is abandoned)
- The resolution oracle sources conflict irreconcilably
- The market question is found to be ambiguous post-hoc
- The creator or a key stakeholder is discovered to have had insider information
On VOID, the algorithm refunds every open position at its cost basis (for buyers) rather than at 50/50, because cost basis more accurately reflects the capital the trader committed. Specifically:
For each (user, market) with nonzero position:
payout = user.yes_cost_kobo + user.no_cost_koboAll escrow is thereby disbursed; positions are zeroed; the market is closed permanently.
7. Liquidity bootstrapping
7.1 The cold start problem
A CLOB with no resting orders is useless. A user who arrives at an empty book and sees no prices has no reason to trade. Sabi Market addresses this with a house market maker (account u_housemm) that seeds every new market with symmetric quotes around the creator's estimated mid price.
On market creation, for a market with expected fair YES probability p*, the seeder places:
buy_yesorders atp* - 1,p* - 2,p* - 3(sizes 200 shares each)buy_noorders at(100 - p*) - 1,(100 - p*) - 2,(100 - p*) - 3(sizes 200 each)
This produces an instant tight book: top-of-book spread of 2 kobo, three levels of depth each side, sufficient for initial retail activity. As soon as any external participant quotes more aggressively, the house MM is undercut and naturally deprioritized by price-time priority.
7.2 Why this doesn't lose money
The house MM is fully collateralized — its orders are backed by its wallet balance in the same way user orders are. When it gets filled, it acquires a position; that position pays out or not based on the actual resolution. Across many markets, the house MM's expected P&L equals the accuracy of its initial mid estimates minus a small spread capture.
In practice the house MM is expected to lose small amounts on individual markets where the seed was mispriced, and gain small amounts where professional MMs arrive and push prices toward fair. The net cost is a modest subsidy the platform pays to bootstrap liquidity — a standard "cost of customer acquisition" that declines as the market matures.
7.3 Professional market makers
Sabi Market offers an API key program for professional MM firms. Key holders get:
- 5× higher rate limits (300 req/min vs 60 for retail)
- Batch order placement (up to 15 orders per call)
- HMAC-signed order submission (proof of intent, no session cookies)
- Maker rebates — makers receive a negative fee (−5 bps) on fills, funded by taker fees (+15 bps). Net platform take: 10 bps.
When professional MMs are active on a market, the house MM withdraws (deprioritizes) to avoid being picked off.
8. The double-entry ledger
8.1 Structure
Every money movement in Sabi Market is recorded as a set of ledger entries that sum to zero. The core table:
CREATE TABLE ledger_entries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
txn_id TEXT NOT NULL, -- groups the offsetting entries
account_id TEXT NOT NULL, -- which account
amount_kobo INTEGER NOT NULL, -- signed; credits positive, debits negative
reason TEXT NOT NULL, -- deposit|trade_mint|settle|etc
ref_type TEXT,
ref_id TEXT,
created_at INTEGER NOT NULL
);Accounts fall into five categories:
| Owner type | Example | Purpose |
|---|---|---|
user | Your wallet | Withdrawable balance |
market_escrow | Per-market | Locked collateral backing outstanding shares + open-order reserves |
system | cash_in | Represents the real bank-held NGN behind all credits |
fee | fees_earned | Trading fees accumulated |
bond | Per-proposal | Resolution proposer/challenger bonds |
8.2 The balance-to-zero invariant
The only invariant that the ledger code enforces at the application layer is:
function post(entries: LedgerEntry[]): void {
const sum = entries.reduce((s, e) => s + e.amountKobo, 0);
if (sum !== 0) throw new Error(`entries do not balance: sum=${sum}`);
// …insert all entries under one txn_id
}This single check, enforced at every insertion point, has a remarkable consequence:
The sum of all `amount_kobo` in the `ledger_entries` table, at any point in time, is exactly zero.
Equivalently: total credits exactly equal total debits. Equivalently: for every kobo credited to any account, there is a matching kobo debited elsewhere. There is no source of value in the system; value only moves.
8.3 Proof of reserves
The ledger invariant lets us compute, at any instant:
total_user_wallet_balances = Σ { balance(a) : a is a user wallet }
total_escrow = Σ { balance(a) : a is a market escrow }
total_bond_pools = Σ { balance(a) : a is a bond }
total_fees_earned = Σ { balance(a) : a is a fee account }
total_system_cash_in = Σ { balance(a) : a is a system account }
By the zero-sum property:
total_user + total_escrow + total_bond + total_fees + total_system = 0
=> total_system = -(total_user + total_escrow + total_bond + total_fees)The system.cash_in account runs negative, representing the real NGN we owe (equivalently: the NGN held in the bank trust account on behalf of all users and markets). Proof of reserves is simply the act of publishing |total_system| and demonstrating that this number matches the trust account's balance at a cooperating bank.
Future roadmap: publish an hourly Merkle root over all user balances and the |total_system| figure to a public log (e.g., a GitHub commit, ultimately an L2 chain). Users can verify their balance was included in the root without revealing other users' balances.
9. Security model
9.1 Custodial, off-chain
Sabi Market is fully custodial — user NGN sits in a Nigerian bank trust account held by a bankruptcy-remote SPV, segregated from operating capital. We considered non-custodial on-chain settlement (Polymarket's model) and rejected it for Nigerian launch because:
- CBN regulations prohibit PSPs (Paystack, Monnify) from routing NGN to on-chain addresses at scale
- Nigerian mass-market users will not manage seed phrases, gas tokens, or wallet recovery
- The regulatory path to SEC Nigeria tolerance runs through the commodity-event-contract framing, which presumes a regulated custodian
We retain the blockchain-grade audit properties (Merkle root publishing, deterministic replay from event log, append-only ledger) without the blockchain-grade UX penalty (seed phrases, gas, chain selection).
9.2 Authentication
A user may sign up via:
- Phone + OTP + PIN — 6-digit numeric OTP via SMS (Termii/Twilio/AWS SNS), 6-digit PIN hashed with scrypt
- Google OAuth 2.0 — PKCE flow with ID token verification against Google's JWKS, then set PIN
Subsequent session management:
- Session tokens are opaque random 32-byte values stored server-side in the
sessionstable - Session cookies are HttpOnly, SameSite=Lax, Secure in production
- Device fingerprint (user-agent + accept-language + IP /24 prefix) binds each session for anomaly detection
- New-device logins generate a
security_eventsrow flagged for review at withdrawal time
9.3 Progressive KYC
Gated on first real-money deposit, not at signup. Borrowed from Polymarket's UX pattern:
- Tier 0 (browser): no account needed, read-only
- Tier 1 (demo / phone-verified): play money for testing, ₦0 real limits
- Tier 2 (BVN-verified via Smile Identity): ₦50k/day deposit, ₦25k/day withdrawal
- Tier 3 (ID + proof of address): ₦1M/day deposit, ₦500k/day withdrawal
Limits align with CBN Tier 1/2/3 AML thresholds.
9.4 Matching-engine security
- Single-writer per market via SQLite
BEGIN IMMEDIATE. No race conditions on book state. - Integer-kobo prices enforced at schema level (
CHECK price_kobo BETWEEN 1 AND 99) - Idempotency keys on every order submission (
client_order_id— unique per user) so that a mobile network drop during order submission cannot double-fill - Rate limiting per user/endpoint (60 req/min retail, 300 MM) and per-IP on auth endpoints (10 OTPs/hour, 15 logins/hour)
- HMAC signatures on API-key-authenticated orders, with nonce + expiration to prevent replay
10. Risk controls
10.1 Per-user controls
- Position limits scale with KYC tier (see §9.3)
- Velocity limits on withdrawals: max ₦500k/day at Tier 2, triggering alert above ₦100k
- 48-hour hold on card deposits (but not bank-transfer deposits) to prevent chargeback fraud
- 2-hour hold on withdrawals to newly-added bank accounts
- Self-exclusion: users can voluntarily freeze their own account for a chosen period
10.2 Per-market controls
- Halt conditions — an admin can halt a market if manipulation is suspected, freezing all new orders while allowing existing holders to continue to resolution
- Position concentration cap — on low-OI markets, no single user may hold more than 25% of open interest on either side
- Creator-trading prohibition — market creators cannot trade on their own markets (prevents insider market creation for personal profit)
10.3 Platform controls
- Daily reconciliation — automated end-of-day reconciliation of the ledger against the bank trust account; drift of any amount pages the on-call engineer
- Hourly ledger audit —
auditLedger()runs every hour in production, raising an alert if anytxn_idgroup sums to nonzero - Append-only audit log — every admin action, auth event, and KYC status change is recorded in the immutable
audit_logtable - Immutable backup — hourly snapshots to S3 Object Lock with 7-year retention (SEC Nigeria requirement)
11. Differences vs Polymarket and Kalshi
| Polymarket | Kalshi | Sabi Market | |
|---|---|---|---|
| Collateral | USDC (on Polygon) | USD (centrally custodied) | NGN (centrally custodied) |
| Price grid | Cent-level, 0.01–0.99 | Cent-level, 1¢–99¢ | Integer kobo, 1–99 |
| Order book | Hybrid CLOB (off-chain match, on-chain settle) | Centralized CLOB | Centralized CLOB (in-process v1; Rust actor v2) |
| Market creation | Permissionless + curation | Operator-only | Community proposals with bonds + curation |
| Identity model | Web3 wallet (embedded via Privy) | Email + KYC | Phone+OTP+PIN OR Google OAuth → PIN |
| KYC gate | On withdrawal | On signup | On first real deposit |
| Resolution | UMA optimistic oracle | Internal settlement committee | Optimistic oracle with bonds (UMA-inspired) + committee escalation |
| Market identity | Content-addressable via keccak256 | Internal ID | Content-addressable via SHA-256 (conditionId) |
| Maker incentives | Rebates, on/off depending on market | Maker fees | Rebates for approved MMs |
| Fees at launch | 0% | 1–2% varies | 0% for 6 months, then 1% taker |
| Mobile app | Native React Native | Native iOS/Android | PWA → Capacitor → native (staged) |
The three systems converge on the same mathematical core (CLOB + conditional tokens + optimistic oracle) and diverge primarily on regulatory positioning, payment rails, and identity. Sabi Market is explicitly the Nigerian adaptation: same math, different wrapper.
12. Roadmap
v1.0 (shipped): Binary CLOB, OTP + Google auth, admin console, progressive KYC UI, optimistic oracle resolution, referral system, 6 seeded flagship markets, Railway deploy.
v1.1 (Q2 2026):
- Real Paystack + Monnify deposits/withdrawals
- Smile Identity BVN + selfie KYC integration
- Vercel + Neon (
af-south-1) migration for African-native latency - WebSocket live order book updates (replacing 2-second polling)
v1.5 (Q3 2026):
- Native iOS/Android apps (Capacitor → Expo)
- Scalar markets (numeric-range forecasts beyond binary)
- Multi-outcome markets (e.g., AFCON winner from 24 teams)
- Merkle-root proof-of-reserves published hourly
v2.0 (2027):
- Rust matching engine, actor-per-market
- Pan-African expansion: KES (Kenya), GHS (Ghana), ZAR (South Africa)
- Optional on-chain mirror (custodial off-chain remains source of truth; chain is a redundant public audit trail)
v2.x (conditional on regulatory clarity):
- SEC Nigeria Designated Contract Market equivalent status
- Institutional FIX API for quant desks
- Options on event contracts (e.g., variance swaps on probability)
13. Conclusion
The algorithm at the heart of Sabi Market is not novel in its primitives — CLOB matching, conditional tokens, optimistic oracle settlement are each well-established. The contribution is the specific composition and adaptation:
- Integer-kobo prices eliminate rounding bugs at the cost of acceptable granularity loss
- Single-writer-per-market matching avoids distributed consensus while remaining correct
- The three-path execution model (share_transfer, pair_mint, pair_burn) captures every possible fill type in a uniform framework
- Double-entry ledger with balance-to-zero invariant provides strong application-level solvency guarantees
- Optimistic oracle with bonds provides economic guarantees against bad resolutions without requiring coordination overhead
- Custodial NGN-native rails meet Nigerian users and regulators where they actually are
The resulting system is simple enough to verify by inspection, strict enough to audit by summation, and fast enough to serve at Nigerian volumes today with a clear path to pan-African scale tomorrow.
Every line of the algorithm is open for scrutiny; the fundamental property — the ledger sums to zero, always — is a fact about the code that any reader can confirm.
Appendix A — Key code references
For readers wanting to verify the specification against the implementation:
| Specification section | Code reference |
|---|---|
| §3 Matching engine | src/lib/engine/matching.ts |
| §4.1 Share transfer | execShareTransferBuy / execShareTransferSell |
| §4.2 Pair mint | execPairMint |
| §4.3 Pair burn | execPairBurn |
| §5 Ledger invariant | src/lib/engine/ledger.ts, post() |
| §5.3 Invariant audit | auditLedger() |
| §6 Optimistic oracle | src/lib/engine/optimistic-resolve.ts |
| §7 House MM seeding | scripts/seed-db.ts |
| §9 Authentication | src/lib/auth.ts, src/lib/oauth-google.ts |
| Condition ID hash | src/lib/engine/condition.ts → computeConditionId |
Appendix B — Glossary
- CLOB: Central Limit Order Book. Matching mechanism where orders are queued by price and time; takers match against the best-priced resting maker.
- Open Interest (OI): The total number of outstanding YES/NO pairs in a market. Equivalent to the dollar value of collateral locked in escrow for that market's shares.
- Pair: One YES share plus one NO share. Has a guaranteed ₦1 payout at resolution regardless of outcome, which is why pairs can be minted out of ₦1 of collateral or burned back to ₦1.
- Maker: The resting order in a match; provides liquidity; receives their quoted price.
- Taker: The incoming order that crosses the spread; consumes liquidity; receives price improvement where applicable.
- Condition ID: Content hash uniquely identifying a market across all regions and time. If any parameter (question, rules, oracle, outcomes, region) changes, the condition ID changes — markets cannot be silently amended.
- Optimistic oracle: Resolution mechanism where outcomes are assumed correct unless challenged within a window, with economic bonds enforcing honest behavior.
This paper describes the Sabi Market v1.0 prediction algorithm. The reference implementation is open for inspection at the code references in Appendix A. For the latest version, implementation changes, and formal security proofs, see the project repository.
© 2026 Sabi Market Limited. Distributable with attribution.