Goal: Take a brand-new user from zero to a completed, paid-out match on Stellar testnet in under 15 minutes — with no real money at risk.
This is the guided, hands-on companion to the end-to-end demo walkthrough. Where the demo is a terse reference, this tutorial is paced for someone seeing Checkmate-Escrow for the first time: every step explains what you are doing, why it matters, and how to know it worked.
By the end you will have:
- ✅ Created a match between two players
- ✅ Funded the escrow with both players' stakes
- ✅ Submitted a verified result and watched the winner get paid automatically
Pick the format that suits you — the content is identical across all three:
| Format | Best for | Where |
|---|---|---|
| 📝 Text | Following along at your own pace | This document |
| 🎬 Video | Watching the full flow first | Video walkthroughs |
| 🧩 Interactive | Checking your understanding | Tutorial quiz & checklist |
🧪 Practice mode (testnet-only). Everything below runs on Stellar testnet using free Friendbot funds. No mainnet keys, no real XLM, nothing to lose. This is the safe sandbox for learning — see Practice mode & local development if you would rather run everything against a local node.
| Tool | Why | Check it works |
|---|---|---|
| Rust (1.70+) | Builds the contracts | rustc --version |
wasm32 target |
Compiles to Wasm | rustup target add wasm32-unknown-unknown |
| Stellar CLI | Talks to the network | stellar --version |
curl |
Funds testnet accounts | curl --version |
| Step | What happens | Approx. time |
|---|---|---|
| Setup | Build, keys, deploy | ~6 min |
| Step 1 | Create a match | ~2 min |
| Step 2 | Deposit funds | ~2 min |
| Step 3 | Check the result & payout | ~3 min |
| Total | < 15 min |
💡 First time only: the contract build (
./scripts/build.sh) can take a few minutes as Rust compiles dependencies. Subsequent runs are fast.
Run these once before Step 1. They mirror the demo walkthrough, condensed here so the tutorial is self-contained.
# 1. Build the contracts
./scripts/build.sh
# 2. Create three testnet identities
stellar keys generate admin --network testnet
stellar keys generate player1 --network testnet
stellar keys generate player2 --network testnet
# 3. Fund them with free testnet XLM via Friendbot
for KEY in admin player1 player2; do
curl -s "https://friendbot.stellar.org?addr=$(stellar keys address $KEY)" > /dev/null
echo "Funded $KEY: $(stellar keys address $KEY)"
done
# 4. Deploy both contracts
ESCROW_ID=$(stellar contract deploy \
--wasm target/wasm32-unknown-unknown/release/escrow.wasm \
--source admin --network testnet)
ORACLE_ID=$(stellar contract deploy \
--wasm target/wasm32-unknown-unknown/release/oracle.wasm \
--source admin --network testnet)
# 5. Initialize both contracts
ADMIN_ADDR=$(stellar keys address admin)
stellar contract invoke --id $ORACLE_ID --source admin --network testnet \
-- initialize --admin $ADMIN_ADDR
stellar contract invoke --id $ESCROW_ID --source admin --network testnet \
-- initialize --oracle $ORACLE_ID --admin $ADMIN_ADDR
# 6. Grab the native XLM token + player addresses for later
XLM_TOKEN=$(stellar contract id asset --asset native --network testnet)
P1_ADDR=$(stellar keys address player1)
P2_ADDR=$(stellar keys address player2)
echo "Escrow: $ESCROW_ID"
echo "Oracle: $ORACLE_ID"📸 Screenshot:
assets/tutorial/00-setup-deployed.png— terminal showing the two contract IDs printed after deployment. See the assets guide for how to add yours.
✅ Checkpoint: you should see two long C... contract IDs. Keep this terminal
open — the $ESCROW_ID, $ORACLE_ID, $XLM_TOKEN, $P1_ADDR, and $P2_ADDR
variables are reused in every step below.
What you're doing: registering a wager on-chain. Player1 proposes a match against Player2, declaring the stake, the token, and which chess game settles it.
Why it matters: the match is the contract's unit of escrow. Nothing can be deposited or paid out until a match exists, and its terms (stake, token, players) are locked in at creation.
MATCH_ID=$(stellar contract invoke --id $ESCROW_ID --source player1 --network testnet \
-- create_match \
--player1 $P1_ADDR \
--player2 $P2_ADDR \
--stake_amount 100000000 \
--token $XLM_TOKEN \
--game_id "abc123xyz" \
--platform Lichess)
echo "Match ID: $MATCH_ID"🔢 Stroops, not XLM. Stellar amounts are integers in stroops:
1 XLM = 10,000,000 stroops. So100000000above is 10 XLM per player.
Confirm it worked:
stellar contract invoke --id $ESCROW_ID --source admin --network testnet \
-- get_match --match_id $MATCH_IDstate: Pending, player1_deposited: false, player2_deposited: false
📸 Screenshot:
assets/tutorial/01-create-match.png— theget_matchoutput showingstate: Pending.
✅ Checkpoint: the match is Pending. Neither player has deposited yet.
🧩 Quick check: Why is the match Pending and not Active?
Answer
A match only becomesActive once both players have
deposited their stake. Creating the match just records the terms; no funds have
moved yet.
What you're doing: each player transfers their stake into the escrow contract.
Why it matters: the escrow holds both stakes so neither player can back out
once the game starts. The match flips to Active the moment the second
deposit lands — that is the on-chain signal that the game may begin.
# Player1 deposits their 10 XLM stake
stellar contract invoke --id $ESCROW_ID --source player1 --network testnet \
-- deposit --match_id $MATCH_ID --player $P1_ADDR
# Player2 deposits their 10 XLM stake
stellar contract invoke --id $ESCROW_ID --source player2 --network testnet \
-- deposit --match_id $MATCH_ID --player $P2_ADDRConfirm the escrow is fully funded:
stellar contract invoke --id $ESCROW_ID --source admin --network testnet \
-- is_funded --match_id $MATCH_ID
# true
stellar contract invoke --id $ESCROW_ID --source admin --network testnet \
-- get_escrow_balance --match_id $MATCH_ID
# 200000000 (2 × 10 XLM in stroops)📸 Screenshot:
assets/tutorial/02-deposit-funded.png—is_fundedreturningtrueand the balance at200000000.
🧠
is_fundedvsget_escrow_balance— these answer different questions and are a common source of confusion:
is_funded→trueonly when both players have deposited (match isActive). It reflects deposit flags, not token balances.get_escrow_balance→ the amount currently held:0,1×stake, or2×stake. After payout/refund it returns0.
✅ Checkpoint: is_funded is true, balance is 200000000, and the match
state is now Active.
🧩 Quick check: After only Player1 deposits, what does each function return?
Answer
is_funded → false (both must deposit), but
get_escrow_balance → 100000000 (one stake is already
held). See the table in the README.
What you're doing: recording the game's outcome and triggering the payout.
Why it matters: this is the trustless core of Checkmate-Escrow. Once the oracle reports a verified result, the escrow pays the winner the entire pot in the same transaction — no platform can withhold or delay it.
The result is settled in two calls:
stellar contract invoke --id $ORACLE_ID --source admin --network testnet \
-- submit_result \
--match_id $MATCH_ID \
--game_id "abc123xyz" \
--platform Lichess \
--result Player1WinsValid
--resultvalues:Player1Wins,Player2Wins,Draw.
The escrow only accepts the result from its configured oracle address (the oracle contract id in this tutorial), never from the admin directly.
stellar contract invoke --id $ESCROW_ID --source $ORACLE_ID --network testnet \
-- submit_result \
--match_id $MATCH_ID \
--winner Player1 \
--caller $ORACLE_IDValid
--winnervalues:Player1,Player2,Draw. On aDraw, both players get their 10 XLM back instead of one winner taking 20 XLM.
Confirm the payout:
stellar contract invoke --id $ESCROW_ID --source admin --network testnet \
-- get_match --match_id $MATCH_ID
# state: Completed, completed_ledger: <ledger number>
stellar contract invoke --id $ESCROW_ID --source admin --network testnet \
-- get_escrow_balance --match_id $MATCH_ID
# 0 (the pot has been paid out)📸 Screenshot:
assets/tutorial/03-result-completed.png—get_matchshowingstate: Completedand the balance back to0.
✅ Checkpoint: the match is Completed and the escrow balance is 0 — the
winner has been paid. 🎉 You've completed a full match lifecycle!
🧩 Quick check: Why is the escrow balance 0 even though the winner was just
paid 20 XLM?
Answer
get_escrow_balance reports funds held in escrow for the match.
Once the payout executes, the pot has left escrow (it is now in the winner's
wallet), so the escrow balance for that match is 0.
Short screen recordings of the full tutorial flow and individual steps. Each walkthrough shows setup, match creation, funding, and payout verification.
| Title | Duration | Status |
|---|---|---|
| Full Tutorial Run — From setup to payout, end-to-end | ~15 min | [Coming Soon] |
| Part 1: Setup & Deploy — Building contracts and initializing on testnet | ~6 min | [Coming Soon] |
| Part 2: Create & Fund a Match — Creating a match and depositing stakes | ~5 min | [Coming Soon] |
| Part 3: Verify Result & Claim Winnings — Oracle submission and automatic payout | ~4 min | [Coming Soon] |
Recordings are produced with OBS Studio or Loom and follow the checklist in docs/assets/tutorial/README.md:
- Consistent window size and zoom level
- Redacted secret keys and private data
- Captions or text overlays for each major step
- Clear audio or screen annotations
We welcome video contributions! To submit:
- Record your walkthrough following the guidelines above
- Save to
docs/assets/tutorial/with a descriptive filename (e.g.,setup-and-deploy.mp4) - Update the table above with your link
- Submit a PR with the video and any new documentation
Everything above runs on testnet, which is practice mode — the funds come from Friendbot and have no real value. Two ways to practice safely:
You already did this. To run again with a clean slate, generate fresh keys
(stellar keys generate player1-v2 --network testnet) and repeat from Setup.
Re-fund any new key via Friendbot.
Prefer an offline sandbox? Run a local Stellar node and point everything at the
standalone network defined in environments.toml:
# Start a local network (see Stellar CLI docs for the current command)
stellar network start local
# Then replace `--network testnet` with `--network standalone` in every command,
# and fund accounts against your local Friendbot instead of friendbot.stellar.org.Local mode resets whenever you stop the node — ideal for repeatedly rehearsing the flow without touching any shared network.
⚠️ Never use mainnet to practice. Mainnet moves real XLM. This tutorial is deliberately testnet/standalone-only.
| Symptom | Likely cause | Fix |
|---|---|---|
account not found / funding errors |
Account not funded on testnet | Re-run the Friendbot loop in Setup |
error: contract not found |
Wrong/empty $ESCROW_ID or $ORACLE_ID |
Re-run deploy; confirm the variables are still set (echo $ESCROW_ID) |
Build fails with a wasm32 error |
Wasm target missing | rustup target add wasm32-unknown-unknown |
deposit fails with an auth error |
Wrong --source for the player |
The --source must match the --player whose stake is being deposited |
Payout (submit_result on escrow) rejected |
Called by admin instead of the oracle |
--source and --caller must both be the oracle address ($ORACLE_ID) |
ContractPaused error |
Admin paused the contract | stellar contract invoke --id $ESCROW_ID --source admin --network testnet -- unpause |
| Variables empty in a new terminal | Shell variables don't persist | Re-export ESCROW_ID, ORACLE_ID, XLM_TOKEN, P1_ADDR, P2_ADDR |
Still stuck? Open an issue or ask in the project's discussions — and please note which step and what command so we can improve this tutorial.
- 🧩 Test your understanding with the tutorial quiz & checklist
- 📖 Read the full demo walkthrough for governance, timeouts, and cancellation flows
- 🏛️ Understand the design in the architecture overview
- 🔮 Learn how results are verified in the oracle design
- 🤝 Ready to contribute? See the contributing guidelines
🔄 Keep this tutorial alive. If a step confused you, that's a documentation bug. Open an issue or PR noting where you got stuck — we update this guide whenever user feedback reveals a rough edge (see the Feedback loop section of the tutorial quiz).