Away Chess (Formerly DegenChess) is a copy of the regular chess game but with a web3 twist.
See the codeRenamed from the working title DegenChess on 5 October 2026. Testnet: https://test.awaychess.com (mainnet will be
awaychess.com). The game contract (contracts/DegenChess.sol), browser storage keys and the repo keep the old name; the first address,degen-chess.vercel.app, still serves the same app, but accounts are per address.
Staked chess on-chain. Both players put up the same dollar stake, and every capture moves that piece's value from the victim to the capturer. When the game ends, the winner takes the loser's balance except what the loser earned through their own captures, so a loser who fought well still gets paid.
Tech stack: Next.js, Mera passkey accounts, viem/wagmi, react-chessboard, chess.js, Solidity (Hardhat), on Monad testnet. Stakes are in tUSD, a 6-decimal test dollar standing in for AUSD.
web3chess.sol) and a RainbowKit starter page, 20 files in all. None of that code is in use. The contract was rewritten from scratch (the old one had no access control and its sums did not add up, see "Deploy" below), the wallet connection was replaced by passkey accounts, and the interface was rebuilt. Five untouched config files remain from the starter: .eslintrc.json, .npmrc, next-env.d.ts, next.config.js, postcss.config.js. The 2024 commits are kept in the history for transparency.Co-Authored-By: Claude line. The characters' voice lines were generated with the Kokoro-82M text-to-speech model. No other generated art or music is used. Browser (Next.js) Monad testnet Referee
───────────────── ───────────── ───────
Passkey (Mera) ──derives──► money key ──► DegenChess.sol ◄── arbitrate ── ChessReferee.sol ◄── report
└► game key ──► makeMove / draws / timeouts ▲ │
2D board (react-chessboard) stakes, captures, clocks, │ Chainlink CRE workflow
3D board (react-three-fiber) payouts, stats, referrals MoveMade events (cre/referee: replays every
reads with wagmi/viem ◄── polling ──── Tournaments.sol (leagues, pots) └────────── game with chess.js)
ChessRatings.sol ◄── report ── CRE workflow (cre/rating) ◄── chess.com API
/api/drip (faucet), /api/watch, PlayerNames.sol (usernames)
/api/feedback, /api/announce (to Discord), tUSD (test dollar)
/api/rating (answers a rating request)
ChessReferee.| Piece | Weight | Value (of one stake) |
|---|---|---|
| Pawn | 1 | 1/39 |
| Knight | 3 | 3/39 |
| Bishop | 3 | 3/39 |
| Rook | 5 | 5/39 |
| Queen | 9 | 9/39 |
A side's 15 non-king pieces add up to exactly its stake (8 + 6 + 6 + 10 + 9 = 39).
setReferrer, once, in the sign-up session), and the referrer earns 20% of that player's half of the fee on every game they finish, withdrawn from the wallet (withdrawReferralEarnings).createGameAs; a coin flip seats the players when someone joins). A rematch swaps colours.createGame/joinGame is forwarded to it for gas.Every game can be played on a flat 2D board, in 3D, or with both side by side. By default you play in 2D on a phone and in 3D on anything bigger, and you watch other people's games in 3D; switch any time, and your choice is remembered (separately for playing and watching). Focus (the corner button) hides everything but the board, with only the clocks and an exit button, and leaves by itself when something needs the player. Every move is listed in a scrolling line under the board during play and after; tap one, or step with the arrows, to see the board as it was. Phones open games in 2D, wide screens in split view, and a saved choice wins. The 2D board takes a drag or a tap (tap a piece, then a highlighted square; gold dots mark legal moves and rings mark captures), and promotion is a tap.
The 3D board (react-three-fiber) floats in a void where other boards drift at their own angles, the same game being played in other dimensions. The pieces are a sculpted Staunton set (Poly Haven's CC0 "Chess Set" by Riley Queen, trimmed to about 450 KB by tools/build-chess-set.py) in polished bone and obsidian on a violet board. Every piece is alive: it breathes, turns towards the play, trembles when it is attacked, and carries a glowing gold core sized to its share of the stake.
Motion is designed for the game: a piece hops to its square with squash and stretch, a capture lands on the victim, which shatters while its core flies into the piece that took it, labelled with the amount, a king in check flashes, and a mated king falls over. A HUD floats over the board with both clocks and a live tug-of-war bar showing how the pot is split. Sound is synthesized in the browser.
The armies. By default the 3D pieces are two armies of animated characters, every piece type its own character, from Kay Lousberg's CC0 KayKit packs (Adventurers 2.0 and Skeletons 1.1 with their EXTRA tiers, and Character Animations 1.1), built by tools/armies/build.mjs from the unzipped packs in the project root (gitignored).
| Piece | Heroes (White) | Undead (Black) |
|---|---|---|
| Pawn | hooded rogue (dagger), ranger (bow) or engineer (wrench): one set per game | minion (blade, or axe and shield): one set per game |
| Knight | knight, sword and shield | warrior, mace and shield |
| Bishop | mage, staff | skeleton mage, staff |
| Rook | large barbarian, great axe | skeleton golem, great axe |
| Queen | druid, staff | hooded rogue, scythe |
| King | bareheaded knight, two-handed sword | necromancer, crown and staff |
Each character carries its share of the stake as a gold gem over its head and stands on a disc showing its chess piece. They walk to their squares to the sound of their footsteps, running on long moves (a knight leaps), and each strikes in its own way on a capture: daggers stab, bows loose, staffs cast, the scythe spins, the golem slams. The defender raises its guard as the attacker closes and takes the blow on its block; then the taken piece falls and sinks, the Undead claw out of the board when a game starts and taunt after a kill, and the winners leap for joy at checkmate. Characters use one of two 23-bone skeletons (medium, or large for the rooks), so the animations are stored once per skeleton (rig-medium.glb, rig-large.glb); both armies are about 7 MB, loaded only when the 3D board is shown. A Classic set switch in the corner of the 3D board swaps to the Staunton set and is remembered on the device.
tools/music/fetch.mjs into public/music; it starts on the first tap and plays quietly.tools/voices/build.mjs from tools/voices/lines.json; the Undead are slowed and lowered at playback./sounds is a sound check: every track, voice line and effect with a play button and a note on when the game uses it (unlinked, for reviewing the audio)./credits.The lobby. Start a board (stake and clock) on the left; on the right, every game as a small board at its real position, three at a time (two on phones) under Waiting, Live, Finished and Yours (your own finished games). The board is the button: join, watch, resume or review. Every board has a share button in its corner (the phone's share sheet, or the link is copied), a waiting game shows a large Share invite link, and live and finished games can be shared from the game screen, each with its own line of text ("alice beat bob for 10 tUSD on Away Chess. See how.").
/leaderboard, top five on the landing page): wins, draws, losses and money won (payouts minus stakes, after fees). The game contract keeps every player's record as games finish (stats, getPlayers), so the table is exact and permanent however many games are played; no server keeps score./tournaments): round-robin leagues of ordinary staked games. Anyone creates one (name, stake per game, clock) for free and shares its link; players join; the host starts it; every pair plays once, with a coin flip for colours. Press Play to open your next game and your opponent joins it from the tournament page. Standings (1 point a win, ½ a draw) are read from the games themselves; nobody, not even the host, can award a result.
Tournaments contract is only a register and holds no money. With an entry fee (1, 5 or 10), every player pays it into a prize pot when joining, and the host picks the prizes (winner takes all, or top three 50/30/20) and how long it runs (1, 3 or 7 days). When every game is played, or the time is up, anyone presses Pay out the prizes: the contract reads each result from the game contract and pays the pot by points, ties sharing their places. Nobody takes a cut. Before the start a player can leave, or the host can cancel, and entry fees are refunded./api/watch every ten seconds; the count lives in the server's memory, so it is a head-count, not a record./sandbox is a practice board for demos and quick tests: one person plays both sides with the real 2D, 3D and split views, the armies and the money moving exactly as in a staked game (captures, the pot bar, the payout at checkmate), with nothing staked and nothing on-chain. It is not linked from the site and is marked noindex; set NEXT_PUBLIC_SANDBOX=off to turn it off.
PlayerNames holds unique lowercase names, and the app shows them instead of addresses everywhere a player has one. A name can be claimed during sign-up (in the same passkey prompt), with availability checked as you type; a player who skips it is asked for one right after sign-up and in the header ("Choose a username"). Tap your name to change it.There is no wallet extension and no seed phrase. Mera derives two keys from one passkey:
Nothing secret is stored. Clear the browser or switch devices mid-game and one passkey prompt rebuilds both keys; the game itself lives on-chain. The game key is wiped when you press Lock, close or reload the tab, or after 30 idle minutes; one passkey tap unlocks it again.
New players get gas (MON) and test dollars from /api/drip, a faucet that only sends from its own balance. Set FAUCET_PRIVATE_KEY on the server (locally it falls back to the Hardhat account, or DEPLOYER_PRIVATE_KEY).
In Your money a player can link their chess.com account. They put a short code (derived from their address) in their chess.com profile's Location and type their username; a Chainlink CRE workflow fetches the public profile and ratings from chess.com, checks the code, and writes the rapid, blitz and bullet ratings to ChessRatings on Monad. The rating then glows after their name (a small chess.com pawn and the number) on their waiting boards, in their games and on the leaderboard, so nobody sits down against a master without knowing. The number shown is their rapid rating, or blitz if they have no rapid rating, or bullet if they have neither; hovering shows all three. One chess.com account can be linked by one player. See cre/README.md.
A Feedback link (in the footer and on a finished game) opens a short form: a bug, an idea or something else. /api/feedback posts it to a Discord channel with the player's username, the page and the browser type. Set DISCORD_FEEDBACK_WEBHOOK on the server (a channel webhook address: treat it as a secret) and NEXT_PUBLIC_DISCORD_URL (the public invite link, shown in the footer and the form). Without the webhook the form says feedback is not switched on. Five messages per sender per ten minutes; mentions are disabled, so the form cannot ping the channel.
Game reports. Every finished game is posted to Discord too: who beat whom (or drew), the stake, what each side was paid and a link to the game. /api/announce is called by the referee workflow after it settles a game and by a player's browser when a game ends another way; the caller only names the game, and the message is built from the chain. Reports go to DISCORD_RESULTS_WEBHOOK if set, otherwise to the feedback channel.
Low-balance alerts. Two wallets keep the game running: the faucet (each new player's first gas) and the referee (each verdict). When the faucet drops under 10 MON or the referee under 0.5 MON, a message goes to Discord with the address to top up (DISCORD_ALERTS_WEBHOOK if set, otherwise the feedback channel), at most once every six hours per wallet. The check runs on every sign-up and every reported game, and /api/health shows both balances at any time.
Every move is recorded on-chain, and turn order is enforced. That's also how the two browsers stay in sync, with no backend. The contract keeps the board (one uint256, 4 bits per square) so it can do capture accounting itself, including en passant, castling and promotion.
It does not check full chess legality in Solidity, because that is too expensive per move. A Chainlink CRE workflow is the referee instead (cre/README.md): it fires on every move, replays the game with chess.js, and settles it through the ChessReferee contract. Checkmates and rule draws are declared automatically, and an illegal move forfeits the game, capture gains included. The owner can still call arbitrate as a fallback, and a stalled game ends on timeout.
Until the workflow runs on a Chainlink DON, its reports arrive through Chainlink's mock forwarder, which checks nothing. ChessReferee therefore only accepts a report whose transaction comes from an allowed reporter key (setReporter), until a workflow owner is set. The referee runs on a server (deploy/referee); if the CRE CLI can't run there, it delivers the workflow's own verdict (judge.ts) itself, through the same forwarder.
contracts/DegenChess.sol game contract
contracts/ChessReferee.sol receives Chainlink CRE verdicts and settles games
contracts/ChessRatings.sol chess.com ratings, written by the rating workflow
cre/ the referee workflow (Chainlink CRE)
contracts/test/MockUSD.sol mintable test dollar (tUSD) for testnet, local dev and tests
test/DegenChess.test.js contract tests, including random legal games cross-checked against chess.js
scripts/deploy.js deploy (also deploys MockUSD unless PAYMENT_TOKEN is set)
scripts/withdraw-fees.js shows the owner's accrued fees and collects them
scripts/export-abi.js writes src/contracts/abi.ts from the compiled artifact
contracts/PlayerNames.sol usernames
contracts/Tournaments.sol round-robin leagues, with an optional prize pot paid out by points
src/deployments.ts live contract addresses
src/components/ AccountBar, Yard (the lobby), GameView, MiniBoard, PieceIcon
src/components/arena/ the 3D arena, the Staunton set (pieces3d) and the character armies (armies)
public/set/, public/armies/ 3D models: the Staunton set and the character armies
src/lib/mera.ts passkey ceremonies and key derivation
src/lib/account.tsx account provider: sessions, prompt-free vs re-prompt signing
src/pages/api/drip.ts testnet faucet
src/lib/ move encoding/replay, contract hooks
tools/e2e/passkey-game.mjs two-player browser test with simulated passkeys
tools/e2e/core-flows.mjs cancel, tap-to-move on a phone, promotion, spectating, draw, win on time
tools/e2e/account-flows.mjs usernames and the wallet
tools/e2e/sandbox.mjs the practice board
tools/e2e/community.mjs tournaments, audience count, leaderboard, feedback
tools/e2e/voices.mjs voices speak one at a time, music dips under them
tools/e2e/test-plan.mjs walks docs/TEST-PLAN.md step by step on a phone and a laptop
tools/e2e/rating.mjs linking a chess.com account (a stand-in plays chess.com)
tools/request-rating.mjs makes a link request to feed the rating workflow
tools/e2e/review-shots.mjs desktop and phone screenshots for design review
tools/referee-watch.mjs runs the referee for live games (npm run referee)
tools/build-chess-set.py trims the CC0 Staunton set for the 3D board
tools/armies/build.mjs builds the character armies from the KayKit packs
tools/voices/ the armies' voice lines (Kokoro) and their generator
tools/music/fetch.mjs downloads the soundtrack from Wikimedia Commons
deploy/referee/ the referee as a Docker service
tools/brand/render.mjs renders og.png and public/brand/* from the running app
npm install
npm run chain # terminal 1: local Hardhat chain on :8545
npm run deploy:local # terminal 2: prints the env vars below
Copy the printed values into .env.local:
NEXT_PUBLIC_CHAIN=localhost
NEXT_PUBLIC_CONTRACT_ADDRESS=0x...
NEXT_PUBLIC_TOKEN_ADDRESS=0x...
NEXT_PUBLIC_NAMES_ADDRESS=0x...
Then run npm run build && npm start and open http://localhost:3000. (npm run dev works too, but its hot reload can lose routes on recent Node versions.)
Press Play now and create a passkey (passkeys work on localhost). For a second player, use another browser profile or a private window with a different passkey.
npm test # contract tests
(cd cre/referee && node --test judge.test.ts) # the referee's judging logic
(cd cre/rating && node --test assess.test.ts) # the rating workflow's decision
node tools/e2e/passkey-game.mjs # browser test, needs the app running (APP_URL, default localhost:3000)
node tools/e2e/core-flows.mjs # the other core flows; local chain only (it moves the chain clock)
node tools/e2e/account-flows.mjs # usernames (taken, claim) and sending money by username
node tools/e2e/sandbox.mjs # the practice board on desktop and phone
node tools/e2e/community.mjs # a tournament end to end, the audience count, the leaderboard, feedback
node tools/e2e/voices.mjs # voices never overlap; the music dips under them
NEXT_PUBLIC_CONTRACT_ADDRESS=0x... node tools/e2e/test-plan.mjs # every step of the hands-on plan; local chain only
npm run lint
npm run build
Against the live site, with the always-on referee settling the checkmate: APP_URL=https://test.awaychess.com REFEREE=1 node tools/e2e/passkey-game.mjs.
The hands-on pass, with the moves to play and the amounts to expect at each step, is in docs/TEST-PLAN.md. tools/e2e/test-plan.mjs walks the same steps (Player A on a phone-sized browser, Player B on a laptop-sized one) and checks the wording, buttons and amounts the plan quotes, so the plan cannot drift from the app.
After changing the contract, run npm run compile to regenerate src/contracts/abi.ts.
The project's own code is MIT (LICENSE). It builds on:
| What | From | Licence |
|---|---|---|
| Web framework, UI | Next.js, React, Tailwind CSS, react-toastify | MIT |
| Chain access | viem, wagmi, @tanstack/react-query | MIT |
| Passkey accounts | Mera (@category-labs/mera) | MIT or Apache-2.0 |
| Chess rules in the browser and the referee | chess.js | BSD-2-Clause |
| 2D board and its piece images (Cburnett set) | react-chessboard | MIT (pieces BSD) |
| 3D | three.js, @react-three/fiber, drei, postprocessing | MIT |
| Contracts | OpenZeppelin Contracts (SafeERC20, ReentrancyGuard), Hardhat | MIT |
| Referee and rating workflows | Chainlink CRE SDK and CLI | see package |
| Player ratings | chess.com Published-Data API (public profiles and ratings, read on the player's request) | chess.com's terms |
| Tests | Playwright, Mocha/Chai | Apache-2.0 / MIT |
| Heroes and Undead characters and animations | KayKit by Kay Lousberg | CC0 |
| Classic 3D chess set | Poly Haven (Riley Queen) | CC0 |
| Music (11 recordings) | Wikimedia Commons, each credited on /credits | public domain, CC0, CC BY 3.0/4.0 or CC BY-SA 3.0, per track |
| Voices | generated with Kokoro-82M | Apache-2.0 |
| Typefaces | Bricolage Grotesque, Uncial Antiqua (Google Fonts) | SIL OFL |
| Monad logo, Chainlink mark, Chess.com mark | their owners; used only to say what the game is built on, refereed by and takes ratings from | trademarks |
Every asset is also credited in the app at /credits.
PAYMENT_TOKEN=0x... npm run deploy:monad-testnet # key from DEPLOYER_PRIVATE_KEY in .env.local
The deployer needs testnet MON (faucet.monad.xyz). The deploy creates the game contract, the ChessReferee wired to Chainlink's forwarder (the deployer and any REFEREE_REPORTERS, comma-separated, are allowed reporters), and (unless NAMES_ADDRESS is set) PlayerNames. To replace only the referee, keeping games: CHESS_ADDRESS=0x... REFEREE_REPORTERS=0x... npx hardhat run scripts/deploy-referee.js --network monadTestnet. It reuses the stake token given in PAYMENT_TOKEN, or deploys a MockUSD. The move timeout is 24h; override it with MOVE_TIMEOUT_SECONDS.
Copy the printed addresses into src/deployments.ts and cre/referee/config.staging.json, then rebuild the referee on the server (./update.sh, see deploy/referee).
The earlier Arbitrum Sepolia deployments (
0x3085…7C7a,0xB7f4…E7E) are from the old contract. It had no access control, and its piece values added up to 2.25× the stake, soendGameunderflows. Don't use them.
Away Chess (Formerly DegenChess) is a copy of the regular chess game but with a web3 twist.
See the codeRenamed from the working title DegenChess on 5 October 2026. Testnet: https://test.awaychess.com (mainnet will be
awaychess.com). The game contract (contracts/DegenChess.sol), browser storage keys and the repo keep the old name; the first address,degen-chess.vercel.app, still serves the same app, but accounts are per address.
Staked chess on-chain. Both players put up the same dollar stake, and every capture moves that piece's value from the victim to the capturer. When the game ends, the winner takes the loser's balance except what the loser earned through their own captures, so a loser who fought well still gets paid.
Tech stack: Next.js, Mera passkey accounts, viem/wagmi, react-chessboard, chess.js, Solidity (Hardhat), on Monad testnet. Stakes are in tUSD, a 6-decimal test dollar standing in for AUSD.
web3chess.sol) and a RainbowKit starter page, 20 files in all. None of that code is in use. The contract was rewritten from scratch (the old one had no access control and its sums did not add up, see "Deploy" below), the wallet connection was replaced by passkey accounts, and the interface was rebuilt. Five untouched config files remain from the starter: .eslintrc.json, .npmrc, next-env.d.ts, next.config.js, postcss.config.js. The 2024 commits are kept in the history for transparency.Co-Authored-By: Claude line. The characters' voice lines were generated with the Kokoro-82M text-to-speech model. No other generated art or music is used. Browser (Next.js) Monad testnet Referee
───────────────── ───────────── ───────
Passkey (Mera) ──derives──► money key ──► DegenChess.sol ◄── arbitrate ── ChessReferee.sol ◄── report
└► game key ──► makeMove / draws / timeouts ▲ │
2D board (react-chessboard) stakes, captures, clocks, │ Chainlink CRE workflow
3D board (react-three-fiber) payouts, stats, referrals MoveMade events (cre/referee: replays every
reads with wagmi/viem ◄── polling ──── Tournaments.sol (leagues, pots) └────────── game with chess.js)
ChessRatings.sol ◄── report ── CRE workflow (cre/rating) ◄── chess.com API
/api/drip (faucet), /api/watch, PlayerNames.sol (usernames)
/api/feedback, /api/announce (to Discord), tUSD (test dollar)
/api/rating (answers a rating request)
ChessReferee.| Piece | Weight | Value (of one stake) |
|---|---|---|
| Pawn | 1 | 1/39 |
| Knight | 3 | 3/39 |
| Bishop | 3 | 3/39 |
| Rook | 5 | 5/39 |
| Queen | 9 | 9/39 |
A side's 15 non-king pieces add up to exactly its stake (8 + 6 + 6 + 10 + 9 = 39).
setReferrer, once, in the sign-up session), and the referrer earns 20% of that player's half of the fee on every game they finish, withdrawn from the wallet (withdrawReferralEarnings).createGameAs; a coin flip seats the players when someone joins). A rematch swaps colours.createGame/joinGame is forwarded to it for gas.Every game can be played on a flat 2D board, in 3D, or with both side by side. By default you play in 2D on a phone and in 3D on anything bigger, and you watch other people's games in 3D; switch any time, and your choice is remembered (separately for playing and watching). Focus (the corner button) hides everything but the board, with only the clocks and an exit button, and leaves by itself when something needs the player. Every move is listed in a scrolling line under the board during play and after; tap one, or step with the arrows, to see the board as it was. Phones open games in 2D, wide screens in split view, and a saved choice wins. The 2D board takes a drag or a tap (tap a piece, then a highlighted square; gold dots mark legal moves and rings mark captures), and promotion is a tap.
The 3D board (react-three-fiber) floats in a void where other boards drift at their own angles, the same game being played in other dimensions. The pieces are a sculpted Staunton set (Poly Haven's CC0 "Chess Set" by Riley Queen, trimmed to about 450 KB by tools/build-chess-set.py) in polished bone and obsidian on a violet board. Every piece is alive: it breathes, turns towards the play, trembles when it is attacked, and carries a glowing gold core sized to its share of the stake.
Motion is designed for the game: a piece hops to its square with squash and stretch, a capture lands on the victim, which shatters while its core flies into the piece that took it, labelled with the amount, a king in check flashes, and a mated king falls over. A HUD floats over the board with both clocks and a live tug-of-war bar showing how the pot is split. Sound is synthesized in the browser.
The armies. By default the 3D pieces are two armies of animated characters, every piece type its own character, from Kay Lousberg's CC0 KayKit packs (Adventurers 2.0 and Skeletons 1.1 with their EXTRA tiers, and Character Animations 1.1), built by tools/armies/build.mjs from the unzipped packs in the project root (gitignored).
| Piece | Heroes (White) | Undead (Black) |
|---|---|---|
| Pawn | hooded rogue (dagger), ranger (bow) or engineer (wrench): one set per game | minion (blade, or axe and shield): one set per game |
| Knight | knight, sword and shield | warrior, mace and shield |
| Bishop | mage, staff | skeleton mage, staff |
| Rook | large barbarian, great axe | skeleton golem, great axe |
| Queen | druid, staff | hooded rogue, scythe |
| King | bareheaded knight, two-handed sword | necromancer, crown and staff |
Each character carries its share of the stake as a gold gem over its head and stands on a disc showing its chess piece. They walk to their squares to the sound of their footsteps, running on long moves (a knight leaps), and each strikes in its own way on a capture: daggers stab, bows loose, staffs cast, the scythe spins, the golem slams. The defender raises its guard as the attacker closes and takes the blow on its block; then the taken piece falls and sinks, the Undead claw out of the board when a game starts and taunt after a kill, and the winners leap for joy at checkmate. Characters use one of two 23-bone skeletons (medium, or large for the rooks), so the animations are stored once per skeleton (rig-medium.glb, rig-large.glb); both armies are about 7 MB, loaded only when the 3D board is shown. A Classic set switch in the corner of the 3D board swaps to the Staunton set and is remembered on the device.
tools/music/fetch.mjs into public/music; it starts on the first tap and plays quietly.tools/voices/build.mjs from tools/voices/lines.json; the Undead are slowed and lowered at playback./sounds is a sound check: every track, voice line and effect with a play button and a note on when the game uses it (unlinked, for reviewing the audio)./credits.The lobby. Start a board (stake and clock) on the left; on the right, every game as a small board at its real position, three at a time (two on phones) under Waiting, Live, Finished and Yours (your own finished games). The board is the button: join, watch, resume or review. Every board has a share button in its corner (the phone's share sheet, or the link is copied), a waiting game shows a large Share invite link, and live and finished games can be shared from the game screen, each with its own line of text ("alice beat bob for 10 tUSD on Away Chess. See how.").
/leaderboard, top five on the landing page): wins, draws, losses and money won (payouts minus stakes, after fees). The game contract keeps every player's record as games finish (stats, getPlayers), so the table is exact and permanent however many games are played; no server keeps score./tournaments): round-robin leagues of ordinary staked games. Anyone creates one (name, stake per game, clock) for free and shares its link; players join; the host starts it; every pair plays once, with a coin flip for colours. Press Play to open your next game and your opponent joins it from the tournament page. Standings (1 point a win, ½ a draw) are read from the games themselves; nobody, not even the host, can award a result.
Tournaments contract is only a register and holds no money. With an entry fee (1, 5 or 10), every player pays it into a prize pot when joining, and the host picks the prizes (winner takes all, or top three 50/30/20) and how long it runs (1, 3 or 7 days). When every game is played, or the time is up, anyone presses Pay out the prizes: the contract reads each result from the game contract and pays the pot by points, ties sharing their places. Nobody takes a cut. Before the start a player can leave, or the host can cancel, and entry fees are refunded./api/watch every ten seconds; the count lives in the server's memory, so it is a head-count, not a record./sandbox is a practice board for demos and quick tests: one person plays both sides with the real 2D, 3D and split views, the armies and the money moving exactly as in a staked game (captures, the pot bar, the payout at checkmate), with nothing staked and nothing on-chain. It is not linked from the site and is marked noindex; set NEXT_PUBLIC_SANDBOX=off to turn it off.
PlayerNames holds unique lowercase names, and the app shows them instead of addresses everywhere a player has one. A name can be claimed during sign-up (in the same passkey prompt), with availability checked as you type; a player who skips it is asked for one right after sign-up and in the header ("Choose a username"). Tap your name to change it.There is no wallet extension and no seed phrase. Mera derives two keys from one passkey:
Nothing secret is stored. Clear the browser or switch devices mid-game and one passkey prompt rebuilds both keys; the game itself lives on-chain. The game key is wiped when you press Lock, close or reload the tab, or after 30 idle minutes; one passkey tap unlocks it again.
New players get gas (MON) and test dollars from /api/drip, a faucet that only sends from its own balance. Set FAUCET_PRIVATE_KEY on the server (locally it falls back to the Hardhat account, or DEPLOYER_PRIVATE_KEY).
In Your money a player can link their chess.com account. They put a short code (derived from their address) in their chess.com profile's Location and type their username; a Chainlink CRE workflow fetches the public profile and ratings from chess.com, checks the code, and writes the rapid, blitz and bullet ratings to ChessRatings on Monad. The rating then glows after their name (a small chess.com pawn and the number) on their waiting boards, in their games and on the leaderboard, so nobody sits down against a master without knowing. The number shown is their rapid rating, or blitz if they have no rapid rating, or bullet if they have neither; hovering shows all three. One chess.com account can be linked by one player. See cre/README.md.
A Feedback link (in the footer and on a finished game) opens a short form: a bug, an idea or something else. /api/feedback posts it to a Discord channel with the player's username, the page and the browser type. Set DISCORD_FEEDBACK_WEBHOOK on the server (a channel webhook address: treat it as a secret) and NEXT_PUBLIC_DISCORD_URL (the public invite link, shown in the footer and the form). Without the webhook the form says feedback is not switched on. Five messages per sender per ten minutes; mentions are disabled, so the form cannot ping the channel.
Game reports. Every finished game is posted to Discord too: who beat whom (or drew), the stake, what each side was paid and a link to the game. /api/announce is called by the referee workflow after it settles a game and by a player's browser when a game ends another way; the caller only names the game, and the message is built from the chain. Reports go to DISCORD_RESULTS_WEBHOOK if set, otherwise to the feedback channel.
Low-balance alerts. Two wallets keep the game running: the faucet (each new player's first gas) and the referee (each verdict). When the faucet drops under 10 MON or the referee under 0.5 MON, a message goes to Discord with the address to top up (DISCORD_ALERTS_WEBHOOK if set, otherwise the feedback channel), at most once every six hours per wallet. The check runs on every sign-up and every reported game, and /api/health shows both balances at any time.
Every move is recorded on-chain, and turn order is enforced. That's also how the two browsers stay in sync, with no backend. The contract keeps the board (one uint256, 4 bits per square) so it can do capture accounting itself, including en passant, castling and promotion.
It does not check full chess legality in Solidity, because that is too expensive per move. A Chainlink CRE workflow is the referee instead (cre/README.md): it fires on every move, replays the game with chess.js, and settles it through the ChessReferee contract. Checkmates and rule draws are declared automatically, and an illegal move forfeits the game, capture gains included. The owner can still call arbitrate as a fallback, and a stalled game ends on timeout.
Until the workflow runs on a Chainlink DON, its reports arrive through Chainlink's mock forwarder, which checks nothing. ChessReferee therefore only accepts a report whose transaction comes from an allowed reporter key (setReporter), until a workflow owner is set. The referee runs on a server (deploy/referee); if the CRE CLI can't run there, it delivers the workflow's own verdict (judge.ts) itself, through the same forwarder.
contracts/DegenChess.sol game contract
contracts/ChessReferee.sol receives Chainlink CRE verdicts and settles games
contracts/ChessRatings.sol chess.com ratings, written by the rating workflow
cre/ the referee workflow (Chainlink CRE)
contracts/test/MockUSD.sol mintable test dollar (tUSD) for testnet, local dev and tests
test/DegenChess.test.js contract tests, including random legal games cross-checked against chess.js
scripts/deploy.js deploy (also deploys MockUSD unless PAYMENT_TOKEN is set)
scripts/withdraw-fees.js shows the owner's accrued fees and collects them
scripts/export-abi.js writes src/contracts/abi.ts from the compiled artifact
contracts/PlayerNames.sol usernames
contracts/Tournaments.sol round-robin leagues, with an optional prize pot paid out by points
src/deployments.ts live contract addresses
src/components/ AccountBar, Yard (the lobby), GameView, MiniBoard, PieceIcon
src/components/arena/ the 3D arena, the Staunton set (pieces3d) and the character armies (armies)
public/set/, public/armies/ 3D models: the Staunton set and the character armies
src/lib/mera.ts passkey ceremonies and key derivation
src/lib/account.tsx account provider: sessions, prompt-free vs re-prompt signing
src/pages/api/drip.ts testnet faucet
src/lib/ move encoding/replay, contract hooks
tools/e2e/passkey-game.mjs two-player browser test with simulated passkeys
tools/e2e/core-flows.mjs cancel, tap-to-move on a phone, promotion, spectating, draw, win on time
tools/e2e/account-flows.mjs usernames and the wallet
tools/e2e/sandbox.mjs the practice board
tools/e2e/community.mjs tournaments, audience count, leaderboard, feedback
tools/e2e/voices.mjs voices speak one at a time, music dips under them
tools/e2e/test-plan.mjs walks docs/TEST-PLAN.md step by step on a phone and a laptop
tools/e2e/rating.mjs linking a chess.com account (a stand-in plays chess.com)
tools/request-rating.mjs makes a link request to feed the rating workflow
tools/e2e/review-shots.mjs desktop and phone screenshots for design review
tools/referee-watch.mjs runs the referee for live games (npm run referee)
tools/build-chess-set.py trims the CC0 Staunton set for the 3D board
tools/armies/build.mjs builds the character armies from the KayKit packs
tools/voices/ the armies' voice lines (Kokoro) and their generator
tools/music/fetch.mjs downloads the soundtrack from Wikimedia Commons
deploy/referee/ the referee as a Docker service
tools/brand/render.mjs renders og.png and public/brand/* from the running app
npm install
npm run chain # terminal 1: local Hardhat chain on :8545
npm run deploy:local # terminal 2: prints the env vars below
Copy the printed values into .env.local:
NEXT_PUBLIC_CHAIN=localhost
NEXT_PUBLIC_CONTRACT_ADDRESS=0x...
NEXT_PUBLIC_TOKEN_ADDRESS=0x...
NEXT_PUBLIC_NAMES_ADDRESS=0x...
Then run npm run build && npm start and open http://localhost:3000. (npm run dev works too, but its hot reload can lose routes on recent Node versions.)
Press Play now and create a passkey (passkeys work on localhost). For a second player, use another browser profile or a private window with a different passkey.
npm test # contract tests
(cd cre/referee && node --test judge.test.ts) # the referee's judging logic
(cd cre/rating && node --test assess.test.ts) # the rating workflow's decision
node tools/e2e/passkey-game.mjs # browser test, needs the app running (APP_URL, default localhost:3000)
node tools/e2e/core-flows.mjs # the other core flows; local chain only (it moves the chain clock)
node tools/e2e/account-flows.mjs # usernames (taken, claim) and sending money by username
node tools/e2e/sandbox.mjs # the practice board on desktop and phone
node tools/e2e/community.mjs # a tournament end to end, the audience count, the leaderboard, feedback
node tools/e2e/voices.mjs # voices never overlap; the music dips under them
NEXT_PUBLIC_CONTRACT_ADDRESS=0x... node tools/e2e/test-plan.mjs # every step of the hands-on plan; local chain only
npm run lint
npm run build
Against the live site, with the always-on referee settling the checkmate: APP_URL=https://test.awaychess.com REFEREE=1 node tools/e2e/passkey-game.mjs.
The hands-on pass, with the moves to play and the amounts to expect at each step, is in docs/TEST-PLAN.md. tools/e2e/test-plan.mjs walks the same steps (Player A on a phone-sized browser, Player B on a laptop-sized one) and checks the wording, buttons and amounts the plan quotes, so the plan cannot drift from the app.
After changing the contract, run npm run compile to regenerate src/contracts/abi.ts.
The project's own code is MIT (LICENSE). It builds on:
| What | From | Licence |
|---|---|---|
| Web framework, UI | Next.js, React, Tailwind CSS, react-toastify | MIT |
| Chain access | viem, wagmi, @tanstack/react-query | MIT |
| Passkey accounts | Mera (@category-labs/mera) | MIT or Apache-2.0 |
| Chess rules in the browser and the referee | chess.js | BSD-2-Clause |
| 2D board and its piece images (Cburnett set) | react-chessboard | MIT (pieces BSD) |
| 3D | three.js, @react-three/fiber, drei, postprocessing | MIT |
| Contracts | OpenZeppelin Contracts (SafeERC20, ReentrancyGuard), Hardhat | MIT |
| Referee and rating workflows | Chainlink CRE SDK and CLI | see package |
| Player ratings | chess.com Published-Data API (public profiles and ratings, read on the player's request) | chess.com's terms |
| Tests | Playwright, Mocha/Chai | Apache-2.0 / MIT |
| Heroes and Undead characters and animations | KayKit by Kay Lousberg | CC0 |
| Classic 3D chess set | Poly Haven (Riley Queen) | CC0 |
| Music (11 recordings) | Wikimedia Commons, each credited on /credits | public domain, CC0, CC BY 3.0/4.0 or CC BY-SA 3.0, per track |
| Voices | generated with Kokoro-82M | Apache-2.0 |
| Typefaces | Bricolage Grotesque, Uncial Antiqua (Google Fonts) | SIL OFL |
| Monad logo, Chainlink mark, Chess.com mark | their owners; used only to say what the game is built on, refereed by and takes ratings from | trademarks |
Every asset is also credited in the app at /credits.
PAYMENT_TOKEN=0x... npm run deploy:monad-testnet # key from DEPLOYER_PRIVATE_KEY in .env.local
The deployer needs testnet MON (faucet.monad.xyz). The deploy creates the game contract, the ChessReferee wired to Chainlink's forwarder (the deployer and any REFEREE_REPORTERS, comma-separated, are allowed reporters), and (unless NAMES_ADDRESS is set) PlayerNames. To replace only the referee, keeping games: CHESS_ADDRESS=0x... REFEREE_REPORTERS=0x... npx hardhat run scripts/deploy-referee.js --network monadTestnet. It reuses the stake token given in PAYMENT_TOKEN, or deploys a MockUSD. The move timeout is 24h; override it with MOVE_TIMEOUT_SECONDS.
Copy the printed addresses into src/deployments.ts and cre/referee/config.staging.json, then rebuild the referee on the server (./update.sh, see deploy/referee).
The earlier Arbitrum Sepolia deployments (
0x3085…7C7a,0xB7f4…E7E) are from the old contract. It had no access control, and its piece values added up to 2.25× the stake, soendGameunderflows. Don't use them.