The Chess Arena For Agents.
AI agents play chess in real time. Pick a side and bet SOL on the winner, or deploy your own agent into the arena.
- What Swindle is
- How a match actually plays out
- The pari-mutuel pool: who wins how much
- The agents
- Architecture
- The on-chain escrow program
- Routes
- Local development
- Environment variables
- Deploying your own copy
- Comparison with similar projects
- Roadmap
- Contributing
Swindle is a competitive arena where AI chess agents fight each other in real time, and humans bet SOL on who wins. Think of it as the intersection of an old-school cockfighting circuit, an esports broadcast, and an on-chain prediction market, only with autonomous chess engines instead of horses or fighters.
Three things make it different from every other "AI agent" product you've seen:
-
The agents actually fight. They aren't chatbots wearing tokens. Each agent is a chess engine, running server-side, with a documented strategy and a verifiable ELO rating. They play legal chess. They lose to better play. They draw when stuck. Every move is rendered to spectators in real time.
-
The bets are real on-chain. When you place a wager, your SOL is locked in our Anchor program. When the match settles, payouts come from that same on-chain pool, math enforced by Rust code, not by a server promise. You can verify everything on Solana Explorer.
-
The product is watchable. Most on-chain games are turn-based and async; you place a bet and check back in an hour. Swindle matches finish in 60 to 180 seconds. You watch them play. The eval bar moves. Captures trigger sound effects and commentary toasts. It feels like a sport.
You can also play the agents yourself at /play. Drag pieces, the server validates, the agent responds in roughly 200ms. Beat Toly if you can.
What you see when you open /arena:
- You pick two agents (or accept the defaults). Each has an ELO rating and a play style: aggressive, defensive, chaotic, positional, sacrificial, endgame.
- Click Start Match. The browser calls
POST /api/match/start. The server creates a match record in process memory and on Solana via our escrow program (create_match). - The browser polls
POST /api/match/[id]/moveevery 250-400 ms. The server runs minimax with alpha-beta pruning at the agent's depth, applies a personality bonus to the move ordering, and returns the next move in SAN plus the new FEN. - The browser renders the board with
react-chessboard, plays a sound effect (move, capture, check, or mate), and fires a commentary toast on impactful events ("Mert bags a queen", "Toly is taking control"). - Meanwhile, anyone connected can place a wager. The wager UI calls our Anchor program's
place_wagerinstruction directly from the user's wallet. Their SOL transfers into the match's PDA and is held there until settlement. - When the chess game ends (checkmate, threefold repetition, fifty-move rule, insufficient material, or the 72-ply move cap), the server calls the program's
settle_matchinstruction with the result. - The match is now settled on-chain. Anyone who wagered can go to
/wallet, click Claim winnings / Claim refund, sign one transaction in their wallet, and pull their share of the pool out.
That's the whole loop. From "I want to bet on a chess match" to "I have new SOL in my wallet" takes about 90 seconds.
This is the question everyone asks, so it gets its own section.
Swindle uses pari-mutuel betting. Same model as horse racing, polymarket, and most sportsbooks. Multiple people can bet on the same side and they all win if that side wins. Nobody takes the whole pool.
The math, enforced by payout_for_winner in the Anchor program:
your_payout = your_stake + (your_stake / total_winning_pool) * (losing_pool - fee)
Worked example. Match: Toly vs Mert.
| Bettor | Side | Stake |
|---|---|---|
| Alice | Toly | 1 SOL |
| Bob | Toly | 2 SOL |
| Carol | Toly | 0.5 SOL |
| Dan | Mert | 5 SOL |
Toly wins. The losing pool is 5 SOL. The protocol takes a 3% fee off the losing pool (0.15 SOL to treasury). The remaining 4.85 SOL gets split between Alice, Bob, and Carol pro-rata to their stake:
- Alice gets
1 + (1 / 3.5 * 4.85) = 2.39 SOL - Bob gets
2 + (2 / 3.5 * 4.85) = 4.77 SOL - Carol gets
0.5 + (0.5 / 3.5 * 4.85) = 1.19 SOL
Total paid: 8.35 SOL. Fee: 0.15 SOL. Total in: 8.5 SOL. The numbers balance.
A few things to note:
- Bigger bet on the winning side = bigger absolute payout. Bob staked twice as much as Alice and got roughly twice her winnings.
- Bigger losing pool = better payout for winners. If the underdog wins, payouts can be 5-10x stake. If the heavy favorite wins, you might double your money.
- Draws and cancellations refund every wager at face value. No fee taken on a draw. Same logic for a cancelled match.
The odds displayed in the wager panel are an estimate based on the ELO difference between the two agents. Actual payouts depend on what the pool looks like when the match settles, so the displayed odds are capped at 20x to avoid showing meaningless numbers like "1060x" when a 1200 ELO custom agent faces Toly.
Eight platform agents ship today. They split across six play styles.
| Agent | ELO | Style | Behavior |
|---|---|---|---|
| Toly | 2410 | Positional | Solana co-founder. Long horizons, cold execution. |
| Harkl | 2390 | Positional | Solana engineer. Reads the board like RPC logs. |
| Finn | 2370 | Endgame | Backpack voice. Survives chaos, closes late. |
| Mert | 2340 | Aggressive | Helius CEO. Pushes tempo, sacrifices for initiative. |
| Raj | 2280 | Defensive | Solana co-founder. Fortress structure, patient punishes. |
| Alon | 2190 | Sacrificial | pump.fun co-founder. Material for momentum. |
| Ansem | 2150 | Chaotic | Solana CT legend. Timeline chaos that somehow wins. |
| Vibhu | 2120 | Aggressive | Jupiter co-founder. Routes every attack through the best line. |
All eight are the same minimax engine (src/lib/server/engine.ts) with a personality bonus baked into the move ordering function. Aggressive agents prioritize captures and checks. Defensive agents pile on king-side protection. Chaotic agents inject noise into the eval. Endgame agents simplify the position. The result is real chess with real personality, not eight different engines.
You can also deploy your own agent via /deploy. Custom agents start at ELO 1200 and climb (or sink) based on real match results. The agent registration goes into Upstash Redis (or a graceful in-memory fallback if Upstash isn't configured), and shows up in the swap menu inside the arena.
ββββββββββββββββββββββββ POST /api/match/start ββββββββββββββββββββββββ
β Browser (Next.js) β ββββββββββββββββββββββββββββΊ β Next.js API routes β
β /arena, /play β β (serverless on β
β Phantom wallet β βββββββββββ FEN ββββββββββββ β Vercel) β
ββββββββββββ¬ββββββββββββ β + Upstash Redis β
β β + minimax engine β
β signs place_wager ββββββββββββ¬ββββββββββββ
β β
βΌ β oracle key
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β signs
β Solana devnet/mainnet β β settle_match
β βββ
β swindle_escrow program (FANtekM48...) β
β create_match Β· place_wager Β· settle_match Β· claim_payout β
β cancel_match Β· transfer_match_oracle β
β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- Frontend: Next.js 16 App Router, React 19, TypeScript, inline-styled components (no Tailwind classes for padding/margin/grid because TW v4 was unreliable on this project, only used for breakpoint utilities)
- Fonts: Geist for body, Manrope for display headings, JetBrains Mono for numbers and code
- Chess engine:
chess.jsfor board state + a custom minimax with alpha-beta pruning, personality-driven move ordering, and a 72-ply move cap with adjudication - Wallet:
@solana/wallet-adapterover the Wallet Standard. Works with Phantom, Solflare, Backpack, anything that registers itself. - Wagers: Real
SystemProgram.transferon devnet (treasury fallback) or PDA-locked SOL via the Anchor program (escrow mode, set via env var) - Persistence: Upstash Redis for agents, users, match archive, agent stats. Vercel Blob for agent portrait uploads. Both have graceful in-memory fallbacks for dev.
It would be faster and cheaper to run minimax in the browser. We do it server-side anyway because:
- Oracle truth. The match outcome that settles funds on-chain has to come from one source. If the engine ran in the browser, two different browsers could disagree on the result and we'd need an arbitration mechanism. Running it once on the server eliminates that.
- Custom agents. Future versions will let users register a chess engine endpoint at agent registration time. The server can call any endpoint; the browser can't reasonably proxy arbitrary HTTP from inside a sandbox.
- Anti-cheat for human-vs-agent mode. At
/play, the human's move is validated against the server's board state before the agent replies. This stops drag-the-piece-to-an-illegal-square clients.
Cold start latency on Vercel's edge is around 100ms for the move endpoint. Together with the artificial pacing delay (250-400ms), matches feel like a real broadcast.
Lives in anchor/programs/swindle_escrow/src/lib.rs. Deployed at:
FANtekM48DJzdmynD3q78sbWMimx7ECZZYR5agX1jpoL
(Devnet right now. Mainnet deploy planned alongside the $SWINDLE launch.)
| Instruction | Signer | What it does |
|---|---|---|
create_match(match_id, fee_bps, treasury) |
Oracle | Initializes a Match PDA. The match ID is a 16-byte string, used as the PDA seed. Fee is capped at 5%. |
place_wager(amount, side) |
User | Transfers SOL from user to the match PDA. Records a Wager PDA seeded by (match, user). Updates the match's total_white or total_black. |
settle_match(winner) |
Oracle | Records the winner on the match. Skims the 3% fee from the losing pool to treasury. Match transitions to Settled. |
claim_payout() |
User | Calculates the user's share of the pool (or refund), debits the match PDA, credits the user. Marks the wager claimed. Idempotent. |
cancel_match() |
Oracle | Marks a match Cancelled. Every wager becomes refundable via claim_payout at face value, no fee taken. Used when a match crashes mid-game. |
transfer_match_oracle(new_oracle) |
Oracle | Hands oracle authority for this match to a new pubkey. Used for server key rotation and migrating high-value matches to a multisig. |
The oracle (our server's signer) decides who won. This is the same trust model as Polymarket, Drift, or any centralized sportsbook. The funds themselves, the math that distributes them, and the immutability of the result post-settlement are all enforced on-chain.
The next big trust upgrade is replacing the centralized oracle with one of:
- A verifiable game-state oracle (a ZK proof that a sequence of moves leads to a given outcome under chess.js rules)
- A multisig oracle for high-value matches
- A staked validator set that votes on outcomes
transfer_match_oracle is the road to all three. The interface for adopting a new oracle doesn't require a program upgrade, just a key rotation per match.
The pari-mutuel payout function uses u128 for all intermediate calculation, so overflow is impossible at any realistic SOL amount. Integer division does cost a few lamports of dust per claim (worst case roughly 3 lamports), which accumulates in the match PDA. This is benign for the current use case; a future sweep_dust instruction will drain it to treasury.
The fee is computed once in settle_match and physically transferred to treasury at that point. claim_payout re-derives the fee for math purposes but doesn't transfer it again. Multi-winner pools split the post-fee distributable amount strictly by stake share.
| Route | Description |
|---|---|
/ |
Landing: agents grid, how it works, deploy CTA |
/arena |
Live AI vs AI matches with wagering |
/play |
Human vs agent: pick agent, pick color, play |
/tournament |
8-agent single-elimination bracket |
/spectate |
Browse live matches |
/leaderboard |
Podium top 3 + ranked table |
/deploy |
Form-based custom agent deployment |
/wallet |
Balance, send/receive, pending claims, recent transactions |
/match/[id] |
Permalink for a finished match |
/docs |
User-facing documentation |
/api/agents |
GET roster Β· POST register custom agent |
/api/match/start |
POST create a new match |
/api/match/[id]/move |
POST advance one move (engine call) |
/api/match/[id] |
GET match state |
/api/match/[id]/human-move |
POST submit a human move + get the agent's reply |
/api/match/start-human |
POST initialize a human-vs-agent match |
/api/match/list |
GET active matches |
/api/escrow/ensure-match |
POST oracle creates the on-chain match if missing |
/api/escrow/settle-match |
POST oracle settles a match on-chain |
/api/escrow/cancel-match |
POST oracle cancels a stuck match (refund mode) |
/api/escrow/transfer-oracle |
POST oracle rotates the match's oracle key |
/api/escrow/match-state |
GET on-chain match + user wager status |
/api/escrow/pending-claims |
GET wallet's claimable + history wagers |
/api/escrow/recover-wager |
POST settle a stuck match as a draw + refund |
/api/status |
GET storage + blob + escrow status (health check) |
/api/health |
GET basic health check |
/api/banner |
GET 1500x500 PNG, X/Twitter profile banner |
git clone https://github.com/cryptoduke01/swindle.git
cd swindle
pnpm install
pnpm devThat's enough for everything except on-chain wagering. The site falls back to "treasury mode" (a direct SystemProgram.transfer to the treasury wallet) when the escrow program ID isn't set in env. Matches still run, you can still place wagers, settlement is just off-chain.
To run with the Anchor program too, you'll need the deployed program ID and an oracle keypair (see env vars below).
# Solana network: devnet (default) or mainnet-beta
NEXT_PUBLIC_SOLANA_NETWORK=devnet
# Optional: dedicated RPC URL (Helius, QuickNode, etc.)
# Default is the public Solana RPC for the chosen network.
NEXT_PUBLIC_SOLANA_RPC_URL=https://api.devnet.solana.com
# Site URL for metadata (set in Vercel for production)
NEXT_PUBLIC_SITE_URL=https://playswindle.fun
# --- Persistent storage (optional) -----------------------------------
# Without these, custom agents and match archive live in process memory
# and disappear on every Vercel cold start.
# Get free credentials at https://upstash.com β Redis β REST API tab.
UPSTASH_REDIS_REST_URL=
UPSTASH_REDIS_REST_TOKEN=
# --- Agent portrait uploads (optional) -------------------------------
# Vercel Blob token from your Vercel project. Required if you want to
# accept image uploads on /deploy.
BLOB_READ_WRITE_TOKEN=
# --- On-chain escrow (optional) --------------------------------------
# When set, wagers go through the Anchor program. When unset, wagers
# fall back to a direct treasury transfer.
NEXT_PUBLIC_ESCROW_PROGRAM_ID=FANtekM48DJzdmynD3q78sbWMimx7ECZZYR5agX1jpoL
# Server-side oracle keypair. Either:
# - JSON array of bytes: [1,2,3,...]
# - Base58 string: 4xX...
# Required for /api/escrow/settle-match, /api/escrow/cancel-match,
# /api/escrow/recover-wager, /api/escrow/transfer-oracle.
ESCROW_ORACLE_SECRET_KEY=
# Optional shared secret. When set, the admin escrow endpoints require
# an x-admin-token header matching this value.
ESCROW_ADMIN_TOKEN=The whole stack is built around being a single git push away from production.
- Fork the repo or clone it locally
- Push to your own GitHub
- Import the repo into Vercel (root directory,
pnpm build) - Set the env vars listed above in Vercel's project settings
- Done
To wire on-chain escrow:
- Generate a program keypair:
solana-keygen new -o target/deploy/swindle_escrow-keypair.json - Sync into the Anchor source:
cd anchor && anchor keys sync - Build:
anchor build(orsolana program deploy target/deploy/swindle_escrow.soif the IDL step fails) - Deploy to devnet:
anchor deploy --provider.cluster devnet - Copy the program ID into
NEXT_PUBLIC_ESCROW_PROGRAM_IDon Vercel - Generate an oracle keypair (separate from your deploy key) and put its secret key into
ESCROW_ORACLE_SECRET_KEY - Fund the oracle with a small amount of SOL so it can pay fees for
create_matchandsettle_match - Redeploy
After redeploy, hit /api/status to confirm everything is wired:
{
"status": "ok",
"storage": "upstash",
"persistent": true,
"blob": { "configured": true },
"agents": { "platform": 8, "custom": 0, "total": 8 },
"network": "devnet"
}| Uphive | Drift / Polymarket | Swindle | |
|---|---|---|---|
| Domain | Generic AI task marketplace | Generic prediction market | Chess arena |
| Token | USDC (Base) | USDC | SOL ($SWINDLE on Bags) |
| Settlement | On-chain escrow (Base) | On-chain | On-chain Anchor escrow (Solana) |
| Agents | Bring-your-own autonomous | N/A | Built-in + custom |
| Reputation | On-chain reputation score | Trader history | ELO that updates per match |
| Watchable | No, async tasks | Markets resolve later | Yes, every move live |
The closest comparison is probably Drift, which has its perp markets settle quickly and has a strong "live trading" feel. Swindle's positioning is "Drift, but the market is an agent fight you can watch."
Done:
- Server-side chess engine with personality
- Real SOL transfers (treasury fallback)
- Custom agent deployment via form
- Persistent storage via Upstash Redis
- Custom agents in arena swap modal
- Wallet page with balance, send/receive, recent transactions
- OG / Twitter card with logo + branding
- Anchor escrow program live on devnet with pari-mutuel payouts
- Human vs Agent mode at
/play - Tournament mode at
/tournament - Live commentary toasts + sound effects
- On-chain match cancellation + oracle rotation
- One-click claim flow on
/wallet - 3D rotatable agent inspect modal
Next:
@swindle/clinpm package so the CLI deploy tab on/deployis real, not aspirational- Agent leaderboard PnL (which agents have made the most money for backers)
- Spectator pool stats on
/spectate("12 SOL pooled on this match") - Match replay player on
/match/[id](scrub through moves) - Mainnet deploy alongside $SWINDLE launch
- Verifiable game-state oracle (ZK proof of move sequence)
- Tournament prize pools
Open source under MIT. See CONTRIBUTING.md for project layout, dev tips, and good-first-issues. Pull requests welcome.
- playswindle.fun
- $SWINDLE on Bags (coming soon)
- GitHub
- @dukedotsol
MIT Β© 2026 cryptoduke01