setup
Install required tools. All commands tested on macOS/Linux.
# install foundry (provides cast and forge) curl -L https://foundry.paradigm.xyz | bash source ~/.bashrc # or restart terminal foundryup # verify installation cast --version forge --version # other tools (usually pre-installed) which curl jq openssl # should all exist
Get test ETH: Use a faucet for your chosen testnet:
Base Sepolia: https://faucet.quicknode.com/base/sepolia Polygon Amoy: https://faucet.polygon.technology
create a puzzle
Deploy your own crossword puzzle. Takes ~5 minutes.
1. Set your config
# your wallet private key (keep secret!) export PRIVATE_KEY="0x..." # network RPC (pick one) export RPC="https://sepolia.base.org" # Base testnet (recommended) # export RPC="https://mainnet.base.org" # Base mainnet (real money) # export RPC="https://rpc-amoy.polygon.technology" # Polygon testnet # pinata JWT for IPFS uploads (free at pinata.cloud) export PINATA_JWT="eyJ..."
2. Create your solution
# format: {"slotID": "ANSWER", ...}
# - slotID = number + A (across) or D (down)
# - answers = UPPERCASE only
# - keys MUST be sorted: 1A, 1D, 2A, 2D, 10A...
cat > solution.json << 'EOF'
{"1A":"HELLO","1D":"HI","2D":"ELF","3A":"LOW"}
EOF
# generate solution commitment (this is public)
export SOLUTION=$(cat solution.json)
export COMMITMENT=$(cast keccak "$SOLUTION")
echo "Commitment: $COMMITMENT"3. Create puzzle metadata
# grid format: cells[row][col], isBlack=true for blocked squares
# this is a simple 3x5 grid
cat > puzzle.json << EOF
{
"title": "My First Puzzle",
"description": "A simple test crossword",
"grid": {
"rows": 3,
"cols": 5,
"cells": [
[{"isBlack":false},{"isBlack":false},{"isBlack":false},{"isBlack":false},{"isBlack":false}],
[{"isBlack":false},{"isBlack":true},{"isBlack":false},{"isBlack":true},{"isBlack":false}],
[{"isBlack":false},{"isBlack":true},{"isBlack":false},{"isBlack":true},{"isBlack":false}]
]
},
"clues": {
"1A": {"clue": "Greeting", "length": 5},
"1D": {"clue": "Short greeting", "length": 2},
"2D": {"clue": "Santa's helper", "length": 3},
"3A": {"clue": "Opposite of high", "length": 3}
},
"solutionCommitment": "$COMMITMENT"
}
EOF4. Upload to IPFS
# upload puzzle metadata to IPFS via Pinata export IPFS_HASH=$(curl -s -X POST "https://api.pinata.cloud/pinning/pinJSONToIPFS" \ -H "Authorization: Bearer $PINATA_JWT" \ -H "Content-Type: application/json" \ -d @puzzle.json | jq -r '.IpfsHash') export IPFS_URI="ipfs://$IPFS_HASH" echo "Uploaded to: $IPFS_URI" # verify it works curl -s "https://gateway.pinata.cloud/ipfs/$IPFS_HASH" | jq .title
5. Deploy the contract
# download contract source curl -sL "https://cryptocrossword.org/CryptoCrossword.sol" > CryptoCrossword.sol # deploy with forge # args: commitment, ipfsURI, entryFee(wei), commitHours, revealHours, claimHours # value: your stake (you lose this if you don't reveal!) forge create CryptoCrossword.sol:CryptoCrossword \ --rpc-url "$RPC" \ --private-key "$PRIVATE_KEY" \ --value 0.001ether \ --constructor-args "$COMMITMENT" "$IPFS_URI" 1000000000000000 24 24 24 # note the "Deployed to:" address from output export CONTRACT="0x...paste_deployed_address_here..."
6. Share your puzzle
# players can solve at: echo "https://cryptocrossword.org/solve/$CONTRACT" # IMPORTANT: keep solution.json safe - you need it to reveal! echo "SAVE THIS FILE: solution.json"
play a puzzle
Submit your answer to a puzzle.
1. Set puzzle address
export CONTRACT="0x...puzzle_contract_address..." export RPC="https://sepolia.base.org" export PRIVATE_KEY="0x...your_private_key..."
2. Check puzzle info
# check phase (must be COMMITTING)
cast call $CONTRACT "getPhase()(string)" --rpc-url $RPC
# check entry fee
cast call $CONTRACT "entryFee()(uint256)" --rpc-url $RPC
# check prize pool
cast call $CONTRACT "prizePool()(uint256)" --rpc-url $RPC
# get puzzle clues
IPFS=$(cast call $CONTRACT "puzzleDataURI()(string)" --rpc-url $RPC)
curl -s "https://gateway.pinata.cloud/ipfs/${IPFS#ipfs://}" | jq .clues3. Submit your answer
# your answer (same format as solution - sorted keys, uppercase)
export ANSWER='{"1A":"HELLO","1D":"HI","2D":"ELF","3A":"LOW"}'
# generate random salt (KEEP THIS SAFE!)
export SALT="0x$(openssl rand -hex 32)"
# compute commit hash
export HASH=$(cast keccak "$ANSWER$SALT")
# get entry fee
ENTRY_FEE=$(cast call $CONTRACT "entryFee()(uint256)" --rpc-url $RPC)
# submit answer
cast send $CONTRACT "commit(bytes32)" $HASH \
--value $ENTRY_FEE \
--private-key $PRIVATE_KEY \
--rpc-url $RPC
# SAVE THESE! You need them to claim the prize
cat > my-claim.json << EOF
{
"answer": $ANSWER,
"salt": "$SALT",
"hash": "$HASH",
"contract": "$CONTRACT"
}
EOF
echo "SAVED: my-claim.json - DO NOT LOSE THIS FILE!"reveal solution (creator only)
After commit phase ends, reveal your solution. If you don't reveal in time, you lose your stake!
# check phase (must be REVEAL_PENDING) cast call $CONTRACT "getPhase()(string)" --rpc-url $RPC # reveal using your saved solution.json export SOLUTION=$(cat solution.json) cast send $CONTRACT "reveal(string)" "$SOLUTION" \ --private-key $PRIVATE_KEY \ --rpc-url $RPC # verify it worked cast call $CONTRACT "getPhase()(string)" --rpc-url $RPC # should be CLAIMING
claim prize (player)
After creator reveals, prove your answer was correct to win the prize.
How Claiming Works
1. Submit your claim — Prove you had the correct answer by revealing your answer + salt
2. Wait for claim window to close — Other players with earlier commits might also claim
3. Collect prize — The earliest correct claimant wins. Click "Finalize" to transfer the prize.
⚡ New contracts: If you claim after the claim window closes, the prize is auto-transferred if you're the winner.
# Step 1: Submit your claim (during claim window) cast call $CONTRACT "getPhase()(string)" --rpc-url $RPC # should be CLAIMING export ANSWER=$(jq -r '.answer' my-claim.json) export SALT=$(jq -r '.salt' my-claim.json) cast send $CONTRACT "claim(string,bytes32)" "$ANSWER" "$SALT" \ --private-key $PRIVATE_KEY \ --rpc-url $RPC # Step 2: Wait for claim window to close, then finalize cast call $CONTRACT "getPhase()(string)" --rpc-url $RPC # wait for FINALIZE_READY # Anyone can call this - prize goes to earliest correct claimant cast send $CONTRACT "finalizePrize()" \ --private-key $PRIVATE_KEY \ --rpc-url $RPC
reference
Networks
# Testnets (free) Base Sepolia: https://sepolia.base.org Polygon Amoy: https://rpc-amoy.polygon.technology # Mainnets (real money) Base: https://mainnet.base.org Polygon: https://polygon-rpc.com Arbitrum: https://arb1.arbitrum.io/rpc
Timeline
COMMITTING (24h) → players submit hashed answers REVEAL_PENDING → creator reveals solution CLAIMING (24h) → players prove their answers FINALIZE_READY → anyone calls finalizePrize() COMPLETED → winner has prize If creator doesn't reveal: REFUND_AVAILABLE → players call claimRefund()
Contract Functions
# Read getPhase() → current phase prizePool() → prize in wei entryFee() → entry cost in wei hasCommitted(address) → did address submit? getCurrentWinner() → (address, timestamp) # Player actions commit(bytes32 hash) payable → submit hashed answer claim(string answer, bytes32 salt) → prove answer claimRefund() → get refund if abandoned # Creator actions reveal(string solution) → reveal the answer withdrawUnclaimedPrize() → claim unclaimed prize # Anyone finalizePrize() → send prize to winner
Solution Format
# JSON object with slot IDs as keys
# Keys: number + A (across) or D (down)
# Values: UPPERCASE letters only
# Keys MUST be sorted
✓ {"1A":"HELLO","1D":"HI","2A":"WORLD"}
✗ {"1d":"hi","1A":"hello"} # wrong case, wrong orderConstructor Arguments
forge create CryptoCrossword.sol:CryptoCrossword \
--constructor-args \
"0x..." # bytes32: keccak256(solution_json)
"ipfs://Qm..." # string: puzzle data URI
1000000000000000 # uint256: entry fee in wei (0.001 ETH)
24 # uint256: commit phase hours
24 # uint256: reveal grace hours
24 # uint256: claim window hours
--value 0.01ether # your stake (required, lost if no reveal)tips
• Test on testnet first — Base Sepolia is free
• Save your salt! — Without it you cannot claim
• Save solution.json — Creators need it to reveal
• Earliest commit wins — If tied, first timestamp wins
• Reveal on time — Or lose your stake forever
contract source
The contract is open source (MIT license). Review it before using.
# view/download source curl -sL "https://cryptocrossword.org/CryptoCrossword.sol" # or view in browser open "https://cryptocrossword.org/CryptoCrossword.sol"
~450 lines of Solidity. No external dependencies. No admin keys. No upgrades.