Why Build a Coinflip on Solana?
A coinflip is the "hello world" of on-chain gaming. It forces you to grapple with randomness, escrow patterns, and PDA-based state management — skills that transfer to every Solana program you will ever write. Plus, Solana's sub-second finality makes the UX feel instant compared to EVM-based alternatives.
In this tutorial you will build a complete coinflip program with Anchor 0.30+ and a minimal TypeScript client that calls it.
Prerequisites
- Rust toolchain (
rustup— stable channel) - Solana CLI v1.18+
- Anchor CLI v0.30+
- Node.js 18+ and
yarnornpm
# Verify your setup
solana --version
anchor --version
rustc --version1. Scaffold the Project
anchor init coinflip
cd coinflipAnchor generates:
programs/coinflip/src/lib.rs— your on-chain programtests/coinflip.ts— Mocha-based integration testsAnchor.toml— config (cluster, program ID, wallet)
2. Define the State Account
Every coinflip round needs persistent state. Create a PDA account that stores the wager, the players, and the result.
use anchor_lang::prelude::*;
declare_id!("YOUR_PROGRAM_ID_HERE");
#[program]
pub mod coinflip {
use super::*;
pub fn create_game(ctx: Context<CreateGame>, wager: u64) -> Result<()> {
let game = &mut ctx.accounts.game;
game.player_one = ctx.accounts.player.key();
game.wager = wager;
game.state = GameState::Waiting;
game.bump = ctx
Using Clock::slot for randomness is not secure for production. Validators can manipulate slot ordering. Use a Verifiable Random Function (VRF) such as Switchboard VRF for real money games.
3. Accounts and State Definitions
#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq, Eq)]
pub enum GameState {
Waiting,
Active,
Resolved,
}
#[account]
pub struct Game {
pub player_one: Pubkey,
pub player_two: Pubkey,
pub winner: Pubkey,
pub wager: u64,
pub state: GameState,
pub bump: u8,
}
impl Game {
4. Build and Deploy
anchor build
anchor keys list # Copy the new program ID
# Update declare_id!() in lib.rs and Anchor.toml
anchor deploy --provider.cluster devnet5. TypeScript Client Integration
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { Coinflip } from "../target/types/coinflip";
import { PublicKey, SystemProgram, LAMPORTS_PER_SOL } from "@solana/web3.js";
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
const program = anchor.workspace.Coinflip as Program<Coinflip>;
async function play() {
const
6. Writing Integration Tests
import { expect } from "chai";
describe("coinflip", () => {
it("creates a game with the correct wager", async () => {
// ... setup from above ...
await program.methods.createGame(wager).accounts({ /* ... */ }).rpc();
const gameAccount = await program.account.game.fetch(gamePda);
expect(gameAccount.wager.toNumber()).to.equal(wager.toNumber
anchor testProduction Considerations
| Concern | Solution |
|---|---|
| Randomness | Replace slot-based flip with Switchboard VRF or Chainlink VRF |
| Front-running | Use a commit-reveal scheme so players cannot see the result before submitting |
| Fee model | Add a small protocol fee deducted before payout |
| Account cleanup | Close the game PDA after resolution to reclaim rent |
| Multi-player | Extend to N-player lottery by generalizing the Game account |
Wrapping Up
You now have a working coinflip program on Solana Devnet. The pattern — PDA state, escrow vault, CPI transfers, pseudo-random resolution — is the foundation for on-chain games, DeFi vaults, and any program that needs trustless two-party settlements.
Check out the Anchor documentation and the Solana Cookbook for deeper dives into advanced patterns like VRF integration and cross-program invocations.