Whitepaper

Felt

A provably-fair, ownerless poker network

Provably fair. No house. Just the game.

Felt is the fully on-chain, ownerless poker network: non-custodial funds, provably-fair deals, and no house to block your withdrawal.

TitleFelt — a provably-fair, ownerless poker network
VersionDraft
Webfelt.poker · app.felt.poker · docs.felt.poker
Sourcegithub.com/felt-poker/felt
Companionarchitecture.md · SECURITY.md · security audit · fairness audit · compliance dossier · deployment

Version: draft. This document describes the protocol as designed. Where a tradeoff is unresolved or a guarantee is only statistical, it says so.


Abstract

Felt is a peer-to-peer poker network with no operator, no house, and no owner — where the only edge is your own play. Cards are dealt using mental poker — EC-ElGamal encryption over secp256k1 under an aggregate key held jointly by the players — so that card secrecy and shuffle fairness hold as long as a single player at the table is honest. Betting and settlement are arbitrated by an immutable smart contract deployed with no admin key: it acts as an optimistic referee (commit-reveal, challenge windows, timeout-fold, bond slashing) and as escrow for player funds. The protocol takes a rake, denominated in basis points, that is paid to block.coinbase — the validator that mines the settlement transaction — so the rake funds transaction inclusion ("mining") rather than any person. No fee-collecting address controlled by the authors exists on-chain. The maintainer's only intended revenue is an off-chain frontend fee in the reference client — never an on-chain skim. That fee is defined but not yet charged (the client currently takes nothing), and it is bypassable by forking a zero-fee client, so the maintainer competes on client quality rather than on protocol ownership. The contracts are plain EVM and deploy immutable to any EVM chain, each chain an isolated liquidity pool; the pool live today is on a testnet (Berachain Bepolia), with parallel-EVM L1s (Monad, Sei, MegaETH) as targets and local Hardhat for development.

1Players escrow their buy-in in the immutable contract2Seats agree an aggregate key P = Σ Pᵢ — no dealer, no server3A verifiable multi-party shuffle encrypts the deck4Cards are dealt encrypted; each seat opens only its own5At showdown, cards open via co-signed decryption shares6An n-of-n-signed settlement pays out on-chain
Figure 1 — The lifecycle of one hand. No step trusts an operator: the key, the shuffle, the deal, and the settlement are all cooperative and verifiable.

Motivation

Why on-chain

Online poker's core problem is trust. A centralized room controls the RNG, holds the money, sees every hole card, and sets the rake. Players must trust that it shuffles fairly, doesn't peek, doesn't front-run, pays out, and doesn't seize funds. History is full of rooms that failed one or more of these. On-chain settlement plus mental poker removes the operator from the trusted set: cards are secured by cryptography the players themselves run, and money moves by immutable contract rather than by an operator's goodwill.

Why ownerless

If you replace the operator with a company that holds an admin key or collects an on-chain rake to its own address, you have rebuilt the house with extra steps — now with a single point of failure and a censorship/seizure lever. This is exactly what custodial crypto rooms do, and it is what a player on Felt never has to trust. Felt's goal is a protocol that keeps running with no one at the controls: no pause, no upgrade, no privileged role, no treasury. The moment two clients can reach the same immutable contract, the network exists. This forces an honest answer to "how does anyone get paid?", which we address in Economics.

Network model

Isolated per-chain pools

The contracts are plain EVM and deploy immutable to any EVM chain — we target parallel-EVM L1s (below) for latency and their own base layer, but nothing binds them to an L1; an EVM L2 works too. Each chain is a separate, isolated pool. Clients connected to chain X form tables, escrow, and settle only with other chain-X clients. There is no cross-chain settlement and no shared liquidity between pools. Adding a chain opens a new pool; it never merges an existing one. The shared infrastructure across all pools is the web application (client + landing) and the signaling relay — the relay is availability-critical shared infrastructure, though it holds no funds and no long-lived state (see architecture.md). The pool deployed today is the Berachain testnet (Bepolia); Monad, Sei and MegaETH are targets, and local Hardhat is the development pool.

We choose parallel-EVM L1s because poker is latency- and throughput-sensitive: many tables settle independent hands concurrently, which parallel execution handles well, and low, stable fees keep the rake economics workable.

N-RPC client orchestration (the client secures the game)

There is no server. The client is the active component and orchestrates a hand from setup to settlement: it drives the mental-poker protocol with peers, constructs and submits transactions, and watches the chain for commitments, challenges, and settlement.

Because liveness and censorship-resistance depend on chain access, the client connects to every RPC endpoint the chain registry lists for its pool, with failover across them (see architecture.md). If an endpoint is slow, wrong-height, or censoring, the client routes around it — where there is more than one to route to: N endpoints per pool is the design, but several registry entries ship a single public endpoint today, and a one-entry list has nothing to fail over to. Each connected client thereby contributes to the security of the games it plays: more honest, well-connected clients mean more independent paths to the chain and more honest key shares at the table.

Card protocol

Cards use mental poker built on EC-ElGamal over secp256k1.

Aggregate key

At table setup, each player i generates a keypair (xᵢ, Pᵢ = xᵢ·G) and publishes Pᵢ with a Schnorr proof of knowledge of xᵢ, in two rounds: every seat first publishes a binding commitment to its share, and only reveals the share once all commitments are in. Both halves are load-bearing. Summing unchecked points would let the last publisher choose P_A = x·G − Σ_{j≠A} P_j and collapse the aggregate to a key it alone holds — reading every hole card for free — and without the commit round it could still pick its share knowing everyone else's. A share that fails either check is rejected and the hand aborts before a card is dealt (docs/protocol/ceremony.md §1). The table's aggregate public key is P = Σ Pᵢ. The 52-card deck is encoded as points and ElGamal-encrypted under P. Because the decryption key is distributed across all players, no one can decrypt a card without every player's cooperation — this is the source of the protocol's n-of-n security.

Verifiable shuffle

The encrypted deck is passed around the table. Each player, in turn, permutes and re-encrypts every card (re-randomizing the ElGamal ciphertexts) and publishes a verifiable shuffle proof — a zero-knowledge argument that the output is a permutation and re-encryption of the input, revealing neither the permutation nor the randomness. After all n players have shuffled, the deck order is unknown to everyone.

Where that proof is checked is the honest part, and it is changing. Every player's client verifies every proof before playing on, and rejects a bad one outright. On-chain there are two regimes:

We state this split rather than claiming the stronger regime, because the difference between "punishable if challenged" and "impossible" is the entire security question for a shuffle.

The mental-poker primitives (aggregateKeys, reEncrypt, shuffleDeck, decryptionShare, combineShares) live in @poker/protocol (packages/protocol/src/mentalPoker.ts); the multiparty ceremony that drives them is @poker/engine. The uniformity of the resulting shuffle is not merely asserted — it is measured and published: a seeded, deterministic simulation of the full ceremony over 200,000 simulated hands passes a chi-square uniformity test at α = 0.001, and the same 200,000-hand run holds with only one honest shuffler; the committed regression test re-runs the audit at 20,000 hands. These are reproducible simulations of the shuffle, not records of played hands (see the fairness audit and the methodology in compliance/fairness.md).

Honest status of the ZK proofs. The statistical audit certifies uniformity given honest re-encryption. The proofs that force honest play are now real code in @poker/zk, at two different maturities — we state each precisely:

We do not claim the shuffle proof is production-audited, and we do not claim it is succinct.

C₀genesis deckπ,ρC₁seat 1π,ρC₂seat 2π,ρCₙshuffled deckeach seat permutes (π) and re-masks (ρ) — proven on-chain
Figure 2 — The verifiable shuffle: the deck passes through every seat, each re-encrypting and permuting it, so no one — and no coalition short of everyone — knows the order.

Randomness from the shuffle

The deal's randomness is the composition of every player's secret permutation. As long as one player shuffles with honest entropy, the final order is unpredictable to all. No VRF, no oracle, and no server RNG is used or needed — introducing one would add a trusted party and a manipulation surface, which is exactly what mental poker avoids.

Dealing and decryption shares

To reveal a card (a player's hole card, or a community card), the players holding the relevant shares publish decryption shares — partial decryptions under their xᵢ, each with a Chaum–Pedersen DLEQ proof of correctness (@poker/zk). For a hole card, the shares are directed so that only the intended player can combine them to recover the card; for community cards, all players contribute shares publicly. An incorrect share is caught by its proof off-chain and is slashable on-chain via challengeShare.

Showdown

At showdown, players reveal the decryption shares for the hands that must be shown. The winner is determined from the opened cards, and the result feeds settlement. Mucked hands need not be revealed. Winner determination uses the canonical hand evaluator in @poker/poker, which is validated exhaustively against all 2,598,960 distinct five-card hands (packages/poker/src/tests/evaluator.exhaustive.test.ts); the same package holds the multiway engine (side pots, min-raise reopening, straddles), Omaha evaluation (best two of four hole cards) and Pot-Limit / Fixed-Limit bet sizing alongside No-Limit, and the rake models (including no-flop-no-drop).

*Trustless showdown when a loser won't cooperate — cards are derived on-chain, not claimed. The paragraph above is the cooperative path: everyone agrees off-chain and co-signs the result. When a contesting loser refuses to co-sign, the showdown is resolved on-chain, from proven cards, trusting no one's word. After a bounded window anyone opens the reveal (PokerRoom.openContestedShowdown); each contender proves its cards with revealShowdownCard, which derives a card from the committed deck plus the players' Chaum–Pedersen decryption shares — it re-checks the deck against the final shuffle commitment, verifies every share's DLEQ, and requires the shares' keys to sum to the aggregate key committed at startHand (Σ Pⱼ == P), then that c2 − Σ Dⱼ == (card+1)·G. Faking a card would therefore mean breaking the table's n-of-n secrecy, not lying to the contract. A seat that will not show is mucked (poker's own rule — don't show, don't win); settleContestedHand then runs the on-chain HandEval evaluator over the proven hands and pays the correct side pots — or voids, settling with no pot movement (each seat keeps its own stake), if no one who could win shows or the board was never fully opened. This card derivation is accept-time sound and replaced a live theft vector in which a finalizer could simply claim* a winning hand; there is deliberately no challengeReveal, because with the cards derived there is no optimistic seam left to challenge. Open finding: the VOID branch slashes nobody. A beaten seat that keeps the board from fully opening (e.g. goes silent so the turn/river shares never land) and then proves its own losing hole cards forces a VOID and recovers its whole stake — so "withholding is a loss" holds for the mucked path, not yet for the VOID path. The construction is unaudited, its soundness rests on the shuffle's validity (N1, above), and the unpriced VOID branch is an open contract finding. See design/sound-reveal-foldin.md.

n-of-n trust, precisely

Card secrecy and shuffle fairness require that at least one player is honest — keeps their key share secret and shuffles fairly. With n players this is a weak, desirable assumption: a coalition of n−1 cheaters still cannot read the honest player's cards or bias the deal. The flip side is a liveness cost: because every player must contribute to reveal a card, any player can stall the protocol. That liveness problem is what the on-chain arbiter's timeout and fallback-settlement machinery exists to bound.

Liveness & disconnection

> The practical experience gaps vs a centralized product (play-time, onboarding, disconnect > handling) and the concrete levers to close them — with a per-hand play-time budget — are > analyzed honestly in strategy/pokerstars-parity.md.

Mental poker's n-of-n secrecy means a single non-cooperating player can freeze a hand. The protocol's aim is to turn "stalling" into a losing move rather than a way to grief. The mechanisms shipped today, and their honest limits:

The bond has an on-chain floor: bond >= bigBlind * MIN_BOND_BB (5), enforced on both table creators. Every punishment above is denominated in the bond, and createTable is permissionless, so without a floor anyone could mint a table with 2 ETH blinds and a one wei bond — and the reference client honours whatever bond a table carries. The floor is set from the two attacks it has to price, rather than picked:

Five is also exactly what the reference client posts (BOND_BB), so the floor refuses no table anyone creates today. It is a floor, not a policy — a table may bond more.

A bond also backs a bounded stack: MAX_STACK_PER_BOND (50) times itself, enforced at join. The floor above anchors on the big blind because TableConfig is all createTable knows. What a seat actually risks is its STACK, and the 40..250 bb window that used to bound that is a client convention the contract never enforced — so a custom client could sit for ten thousand big blinds against a 5 bb bond and drive every punishment to 0.05% of the stack, on a table whose config is perfectly legal. Fifty is exactly the reference client's own ceiling (5 bb bonded, 250 bb deep), so it refuses no buy-in anyone can make through the shipped client. Depth is a property of the table: to sit deeper, sit where the bond is larger. It is checked once, in the single path all four join variants share.

⚠️ One limit remains, stated rather than hidden. Within a table the ratio still varies — at 40 bb the bond is 12.5% of a stack, at the 250 bb ceiling 2% — so the same sabotage costs relatively less against a deeper seat. The cap bounds that spread; it does not remove it. And the 2⁻³² soundness above is the design figure for the cut-and-choose, not an audited one (N1); the hand-rolled secp256k1 the verifiers share is likewise unaudited (N3).

Tournaments price this differently and the floor does not apply to them. An MTT bond is a flat amount posted once at entry (manifest.bond) and returned with the prize — it never passes through createTable. Because blind levels escalate while that bond does not, its value in big blinds decays over the tournament, i.e. the deterrent is weakest exactly when prize equity is highest. Worse, the payoff differs in kind: in a cash game a voided hand refunds and the saboteur simply loses bond, whereas in a tournament a voided hand preserves the saboteur's stack, which converts directly into prize equity for a player about to lose a big pot. This is an open design question, not a resolved one, and it is not covered by the tournament suite today.

On-chain arbiter

The contract is an optimistic arbiter, not an executor of the card protocol. It does not shuffle or decrypt; it holds commitments and adjudicates disputes.

Keeping the arbiter optimistic keeps gas low: the expensive verification of shuffle/decryption proofs only happens on-chain in the rare case of a challenge, not on the happy path.

Economics

Rake to block.coinbase funds mining

The settlement contract skims a rake in basis points (hard-capped at 5%, a constant with no setter) and credits it to block.coinbase — the validator that mines the settle transaction — via a pull-payment (rakeAccrued[block.coinbase], collected later with claimRake). A settle therefore moves no rake ETH itself; it only writes to a mapping, so a validator that cannot or will not accept ETH can no longer revert the settlement (audit F-3). This is deliberate:

The rake→coinbase model applies to native tables only. ERC-20 tables take no on-chain rake (rakeBps is forced to 0 at creation, else the table cannot be created), so the contract never makes a rake promise it cannot honor for arbitrary tokens; the maintainer's cut on token tables is the off-chain frontend fee, exactly as for native tables.

Table currency (native + permissionless ERC-20)

A table declares its currency once, immutably, when it is created:

Three token-safety guardrails make arbitrary-token support safe for everyone except the opt-in players of a given token table:

1. Per-table isolation. Escrow is accounted per (table, player) and paid in the table's own currency, so a malicious/broken token on table A cannot affect table B (different token) or a native table. This is the core safety boundary. 2. Balance-delta accounting. On a token deposit the contract measures its own token balance before and after transferFrom and credits the player the actual delta received, never the requested amount. A fee-on-transfer/deflationary token therefore just gives its user a slightly smaller stack; the contract never pays out more of a token than it holds (pot conservation by construction). 3. Safe transfers + reentrancy discipline. Token moves use a safe wrapper that tolerates non-standard ERC-20s (e.g. USDT, which returns no bool) and rejects codeless targets; every token path is checks-effects-interactions and nonReentrant.

Rebasing tokens are unsupported — static per-player credits cannot track a balance that moves on its own. Per-table isolation confines that risk to the rebasing table's own players. The residual risk of an arbitrary-token table (a rug/broken token) is borne only by the players who chose to sit at it.

Cold-start

A pool is only useful once validators have reason to include its transactions and players have reason to sit. Bootstrapping a new pool may require initial funding/liquidity to seed tables and make early settlement attractive — a launch concern, not a permanent subsidy. The steady state is rake-funded inclusion plus organic table liquidity.

The frontend fee is how the maintainer is designed to be paid

Someone has to build and run a good client, and they deserve to be paid. The intended payment is an off-chain frontend fee charged by the reference web client — a software/service fee that goes to the maintainer. It is not an on-chain rake and does not make the maintainer the house. Status: designed, not charged. The reference client defines the fee (apps/client/src/config/fee.ts) but wires it to nothing — no code path charges it today, so the maintainer currently earns nothing from play. The on-chain half of the model holds regardless: rake goes to block.coinbase and no maintainer-controlled address exists on-chain.

Crucially, the fee is bypassable by design: anyone can fork the client, set the fee to zero, and settle hands on the same immutable pools. The protocol does not know or care which client submitted a hand. This is the honest structure: you earn by shipping the best client (a service), not by owning the protocol (a rent). The competitive moat is client quality — UX, reliability, RPC orchestration, stats, support — not a privileged position on-chain.

Why this stays ownerless

The two revenue ideas that would make us "the house" — an admin key and an on-chain rake to our address — are both absent by construction. Validators are paid by the protocol; the maintainer is paid by users who choose the reference client. Remove the maintainer and the network still runs; remove a validator and another mines the settle tx. No single party is load-bearing.

Anti-collusion & fairness

The protocol guarantees, cryptographically: card secrecy, shuffle fairness, correct settlement, and punishment of provable in-protocol cheating. It cannot cryptographically prevent players from colluding out-of-band — sharing hole cards over a side channel, or one person controlling several seats (multi-accounting). No mental-poker scheme can; this is a social/identity problem, not a cryptographic one, and we will not pretend otherwise.

The available defense is statistical and after-the-fact — and it sees only what reaches the chain. On a cooperative hand, the chain records the settlement outcome (players, pot, rake, winners) plus the hand's commitments; per-street actions, hole cards and the board are not recorded on-chain — they live off-chain in the players' signed transcripts (the client's own on-chain hand-history replay states exactly this). Public on-chain data therefore supports outcome-level analysis — chip flows between accounts, implausibly correlated win rates, chip dumping visible in settlement patterns — while per-action analysis needs histories the clients themselves hold. This detection lives in clients and analytics tooling, not in contract state (the contract stores no stats). It is a deterrent and a signal, not a cryptographic guarantee. This is the honest limit of the system.

(By contrast, the fairness of the deal itself — shuffle uniformity and correct hand ranking — is provable, not merely deterred; see the fairness audit.)

Bots welcome

Because Felt is permissionless, it cannot ban automated players — and does not pretend to. A bot is just another client: it connects to the same chains and relay, runs the same n-of-n ceremony, posts the same bond, and settles through the same immutable contract, with no privileged access and no information a human at the same seat lacks. We make no claim that bots are undetectable or profitable; we state plainly that humans may face bots and that all clients are equal under the protocol. Embracing bots transparently — same information, same bonds, same anti-grief and timeout-fold rules — is more honest than the unwinnable ban war custodial sites fight. The open bot API is @poker/bot-sdk; see Build a bot on Felt.

Felt is a decentralized protocol and a reference client. It is not a licensed gambling operator and makes no claim to any regulatory license, registration, or approval. There is no operator that takes custody of funds, runs the house, or adjudicates games — immutable contracts do those things, and no one controls them. Real-value poker is regulated very differently across jurisdictions and is prohibited in some. Anyone who deploys, hosts, promotes, or plays is solely responsible for compliance with the law that applies to them. The maintainers publish software; they do not operate a gambling service.

The path to a regulated market is Path C: a licensed operator company runs the reference client (charging the frontend fee, performing KYC + geo-block + responsible gaming via the client's compliance anchors) while the protocol stays permissionless and non-custodial. That operator, not the protocol, holds the licence and carries the obligations. The compliance dossier — including the licence-readiness matrix — is the artifact for approaching gaming regulators and certifiers.

Roadmap

Indicative, not a promise of dates. What is done is marked; the rest is pending.


See also: docs index · architecture.md · SECURITY.md · security audit · README.md · fairness audit · compliance dossier · deployment.