A trustless chess wagering platform built on Stellar Soroban smart contracts. Players stake tokens before a match, and the winner is automatically paid out the moment the game ends — no middleman, no delays, no trust required.
Checkmate-Escrow combines competitive chess with Stellar's fast settlement to create a fully on-chain betting platform for casual and high-stakes matches.
Players:
- Stake tokens into a Soroban escrow contract before a match begins
- Play their game on Lichess or Chess.com as normal
- Receive automatic payouts the instant the match result is verified on-chain
A custom Oracle bridges the Chess.com / Lichess API to the smart contract, verifying match results and triggering payouts without any manual intervention.
This makes Checkmate-Escrow:
✅ Trustless (no platform can withhold or delay winnings)
✅ Transparent (all stakes and payouts are verifiable on-chain)
✅ Instant (Stellar's fast finality means payouts settle in seconds)
✅ Accessible (anyone with a Stellar wallet can participate)
- Create a Match: Set stake amount, token address, and link a Lichess/Chess.com game ID
- Flexible Token Support: Any Stellar token address is accepted by default; once the admin adds at least one token via
add_allowed_token, only allowlisted tokens are accepted for new matches - Escrow Stakes: Both players deposit funds into the contract before the game starts
- Oracle Integration: Real-time result verification via Lichess/Chess.com APIs
- Automatic Payouts: Winner receives the full pot the moment the result is confirmed
- Draw Handling: Stakes are returned to both players in the event of a draw
- Admin Controls: Pause/unpause, oracle rotation, admin transfer, and match timeout configuration
- Transparent: All escrow balances and payout history are verifiable on-chain
Matches move through the following states:
Pending ──► Active ──► Completed
│ ▲
└──► Cancelled ◄─────────
(expire_match / cancel_match)
| State | Description |
|---|---|
Pending |
Match created; awaiting deposits from both players |
Active |
Both players have deposited; game is in progress |
Completed |
Oracle submitted result; payout executed |
Cancelled |
Cancelled before activation, or expired after timeout |
| Topic (namespace / name) | Emitted by | Payload |
|---|---|---|
escrow / init |
initialize |
(oracle_address, admin_address) |
admin / paused |
pause |
() |
admin / unpaused |
unpause |
() |
admin / oracle_up |
update_oracle |
(old_oracle, new_oracle) |
admin / xfer |
transfer_admin |
(old_admin, new_admin) |
match / created |
create_match |
(match_id, player1, player2, stake_amount) |
match / completed |
submit_result |
(match_id, winner, payout_amount) |
match / cancelled |
cancel_match |
match_id |
match / expired |
expire_match |
match_id |
New to Checkmate-Escrow? Start with the Local Development Setup guide for step-by-step instructions on building, testing, and running the full stack locally — including running the oracle service locally against a mock Lichess/Chess.com server.
- Rust (1.70+)
- Soroban CLI
- Stellar CLI
./scripts/build.sh./scripts/test.shCopy the example environment file:
cp .env.example .envConfigure your environment variables in .env:
# Network configuration
STELLAR_NETWORK=testnet
STELLAR_RPC_URL=https://soroban-testnet.stellar.org
# Contract addresses (after deployment)
CONTRACT_ESCROW=<your-contract-id>
CONTRACT_ORACLE=<your-contract-id>
# Oracle configuration
LICHESS_API_TOKEN=<your-lichess-api-token>
CHESSDOTCOM_API_KEY=<your-chessdotcom-api-key>
# Frontend configuration
VITE_STELLAR_NETWORK=testnet
VITE_STELLAR_RPC_URL=https://soroban-testnet.stellar.orgNetwork configurations are defined in environments.toml — see the environments.toml reference in the local development guide for a full field-by-field breakdown of each network:
testnet— Stellar testnet (free funds via Friendbot, recommended for development)mainnet— Stellar mainnet (real funds, production only)futurenet— Stellar futurenet (preview of upcoming protocol features)standalone— Local development node (fully isolated, no external connectivity)
# Configure your testnet identity first
stellar keys generate deployer --network testnet
# Deploy
./scripts/deploy_testnet.shFollow the step-by-step guide in demo/demo-script.md
Brand new to Checkmate-Escrow? The Interactive Tutorial takes you from zero to a completed, paid-out match on testnet in under 15 minutes — no real funds at risk:
- Create a match — register a wager on-chain
- Deposit funds — fund the escrow
- Check the result — verify the outcome and watch the payout
Prefer to learn by watching or testing yourself?
- 🎬 Video walkthroughs: the tutorial includes a Video walkthroughs section (links added as recordings are published)
- 🧩 Interactive quiz & checklist: tutorial-quiz.md
- 🧪 Testnet practice mode: the whole tutorial runs on free testnet funds — no real money at risk
- Changelog — release history and notable changes
- Interactive Tutorial — guided, hands-on intro for new users
- Tutorial Quiz & Checklist — verify your understanding
- FAQ — common questions and answers
- Glossary — key terms (escrow, oracle, match states, Soroban, wave-ready, and more) for new contributors
- Architecture Overview
- Oracle Design — comprehensive oracle documentation covering result verification, consensus, and platform integration:
- Lichess Integration — API setup, game ID format, result mapping
- Chess.com Integration — API key setup, rate limits, result field mapping, error handling
- Platform Comparison Table — Lichess vs Chess.com API differences
- WebSocket API — real-time match event protocol (protocol v1), connection lifecycle, subscription model, React hook
- Threat Model & Security
- Security Policy & Vulnerability Disclosure — how to report vulnerabilities, response SLA, and bug bounty
- Balance-Privacy Model — what the balance-snapshot APIs hide from non-admins, and what they don't
- Roadmap
- Deployment Guide
- Mainnet Deployment Checklist — step-by-step checklist for safe mainnet launches
- Monitoring Setup — Prometheus metrics, Grafana dashboard, and alerting rules
- Error Codes Reference — every contract error code, its cause, and how to recover
create_match(player1, player2, stake_amount, token, game_id, platform) -> u64
get_match(match_id) -> Match
cancel_match(match_id, caller) -> Result<(), Error>
expire_match(match_id) -> Result<(), Error>
get_player_matches(player) -> Vec<u64>
get_pending_matches() -> Vec<Match>
get_active_matches() -> Vec<Match>
create_matchmust be authorized byplayer1.cancel_matchmay be called by either matched player.expire_matchmay be called by anyone once the match timeout elapses.
deposit(match_id, player) -> Result<(), Error>
is_funded(match_id) -> Result<bool, Error>
get_escrow_balance(match_id) -> Result<i128, Error>
These two functions answer different questions and are easy to confuse:
-
is_funded(match_id)— returnstrueonly when both players have deposited their stake (i.e. the match has transitioned toActive). It reflects deposit flags, not token balances. Use this to gate game-start logic. -
get_escrow_balance(match_id)— returns the total token amount currently held in escrow for the match:0,1×stake, or2×stakedepending on how many players have deposited. Once a match isCompletedorCancelled(funds already paid out or refunded), this always returns0regardless of on-chain token balances.
Examples:
| Scenario | is_funded |
get_escrow_balance |
|---|---|---|
| Only player1 deposited | false |
1 × stake_amount |
| Both players deposited (Active) | true |
2 × stake_amount |
| Match completed (payout done) | true |
0 |
| Match cancelled (refunds done) | false |
0 |
get_contract_version() -> String
- Returns the crate version from
Cargo.toml(e.g."0.1.0") as a semver-formatted string. Clients can use this to detect version mismatches against a deployed contract.
get_oracle_address() -> Address
submit_result(match_id, winner, caller) -> Result<(), Error>
submit_result_with_oracle_record(match_id, winner, game_id) -> Result<(), Error>
submit_result_batch(results: Vec<(match_id, winner)>, caller) -> Result<Vec<Option<Error>>, Error>
-
get_oracle_address()returns the oracle address currently configured on the contract. This is a view function that requires no authentication and is intended for off-chain clients, frontends, and monitoring tools to verify oracle configuration without reading raw contract storage. ReturnsError::Unauthorizedif the contract has not been initialized. -
submit_result_batchsettles multiple matches in a single call, reducing oracle transaction overhead.callermust be the configured oracle. Each match is processed independently — a failure on one (e.g.NotFunded,InvalidState) does not stop the rest from being processed. The returnedVechas one entry per input entry, in the same order:Noneon success,Some(Error)on failure for that match. (Soroban's contract ABI has noResultelement type, soOption<Error>is the on-chain equivalent ofResult<(), Error>here.) -
submit_resultis called by the configured oracle address and requires oracle authorization. -
submit_result_with_oracle_recordis the canonical oracle integration path and stores the verifiedgame_idfor audit.
submit_result verifies the caller, records the winner, and immediately executes the payout (or refund on draw) in a single transaction. There are no separate verify_result or execute_payout functions.
set_match_timeout(seconds) -> Result<(), Error>
get_match_timeout() -> Result<u64, Error>
set_maximum_stake(amount: Option<i128>) -> Result<(), Error>
set_protocol_config(config: ProtocolConfig) -> Result<(), Error>
get_protocol_config() -> Result<ProtocolConfig, Error>
All admin-control functions below require authorization from the configured admin address.
- Match timeout —
set_match_timeoutsets how long (in seconds) aPendingmatch may wait for both deposits before anyone can callexpire_matchon it. Value must fall within[MIN_MATCH_TIMEOUT_SECONDS, MAX_MATCH_TIMEOUT_SECONDS](1–90 days), enforced withError::InvalidTimeout.get_match_timeoutreturns the current value in seconds. The timeout is stored inProtocolConfig::match_timeout_secondsand defaults toDEFAULT_MATCH_TIMEOUT_SECONDS(30 days). - Maximum stake —
set_maximum_stakecaps thestake_amountaccepted bycreate_matchand its variants. PassSome(amount)to set a cap (rejected withError::InvalidAmountifamount <= 0), orNoneto remove the cap (the default).create_matchrejects anystake_amountabove the configured cap withError::InvalidAmount. Read the current cap viaget_protocol_config().maximum_stake. - Protocol fee —
ProtocolConfig::protocol_fee_bps(basis points, default0) andProtocolConfig::fee_recipientcontrol a platform fee taken out of the winner's payout:protocol_fee = stake_amount * 2 * protocol_fee_bps / 10_000, transferred tofee_recipientwhensubmit_resultresolves a winner. Draw refunds are never charged this fee. Set both fields viaset_protocol_config;protocol_fee_bpsabove10_000(100%) is rejected withError::InvalidAmount. - Full config read —
get_protocol_configreturns the completeProtocolConfigstruct as currently stored on-chain (treasury, vesting duration, cancellation fee, stablecoin-only mode, maximum stake, match timeout, protocol fee, and fee recipient). Off-chain tools and the frontend can call this single function instead of making multiple separate admin queries to reconstruct contract state.
Comprehensive test suite covering:
✅ Match creation and configuration
✅ Deposit validation and escrow locking
✅ Oracle result submission and verification
✅ Winner payout and draw refund logic
✅ Cancellation and edge cases
✅ Error handling and security checks
Run tests:
cargo testThe Problem: Current chess betting and tournament prize payouts are slow and rely entirely on the platform's honesty. Players have no guarantee their winnings will be paid out fairly or on time.
The Solution: By holding stakes in a Soroban smart contract and automating payouts via a verified Oracle, Checkmate-Escrow removes the need to trust any third party.
Blockchain Benefits:
- No platform can withhold or manipulate payouts
- Transparent stake and payout history for every match
- Programmable rules enforced by smart contracts
- Accessible to anyone with a Stellar wallet
Target Users:
- Competitive chess players looking for trustless wagering
- Chess clubs and tournament organizers
- Casual players wanting skin-in-the-game matches
- Developers building on Stellar/Soroban
- v1.0 (Current): Token-allowlist escrow, Lichess Oracle integration, basic match flow
- v1.1: Chess.com Oracle, expanded token support
- v2.0: Multi-game tournaments, bracket payouts
- v3.0: Frontend UI with wallet integration
- v4.0: Mobile app, ELO-based matchmaking, leaderboards
See docs/roadmap.md for details.
We welcome contributions! Please:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
See our Code of Conduct and Contributing Guidelines.
This project participates in Drips Wave — a contributor funding program! Check out:
- Wave Contributor Guide — How to earn funding for contributions
- Wave-Ready Issues — Funded issues ready to tackle
- GitHub Issues labeled with
wave-ready— Earn 100–200 points per issue
Issues are categorized as:
trivial(100 points) — Documentation, simple tests, minor fixesmedium(150 points) — Oracle helpers, validation logic, moderate featureshigh(200 points) — Core escrow logic, Oracle integrations, security enhancements
This project is licensed under the MIT License — see the LICENSE file for details.
- Stellar Development Foundation for Soroban
- Lichess for their open API
- Chess.com for their developer platform
- Drips Wave for supporting public goods funding