Technical Documentation
Technical Documentation
Tech Stack
Blockchain
Avalanche C-Chain (Fuji testnet)
EVM-compatible, sub-second finality, low fees, USDC native support
Smart Contracts
Solidity 0.8.x, Hardhat, OpenZeppelin
Industry-standard tooling; OZ provides audited ReentrancyGuard, SafeERC20, UUPS proxy
Smart Wallets
ERC-4337 via ZeroDev Kernel v0.3.1
Account abstraction lets users sign with passkeys instead of seed phrases
Bundler/Paymaster
Pimlico
Sponsors gas so users never need AVAX; batches approve+deposit into one operation
Frontend
Next.js 15, React 19, TypeScript, Tailwind CSS
App Router for server components, React 19 for concurrent features
Backend
Express 5, TypeScript, PostgreSQL
Lightweight API layer for off-chain metadata and user accounts
Indexing
The Graph (subgraph)
Indexes on-chain events for queryable wager history
Auth
WebAuthn passkeys + email/password fallback
Passkeys for wallet signing; email/password for account recovery
Monorepo
pnpm workspaces
Shared types across frontend, backend, and contracts
Architecture Decisions
1. Immutable Escrow + Upgradeable Governance
The core escrow contract (WagerEscrow.sol) is deployed once and never upgraded. This is intentional. Users need to trust that the rules governing their funds won't change after they deposit. All fund custody, settlement logic, and withdrawal mechanics are baked into this immutable contract.
Governance parameters (fee rates, pause states, mediator management) live in a separate contract (WagerAccessControl.sol) deployed behind a UUPS proxy. This lets the protocol adjust fees or pause specific operations in an emergency without touching the escrow logic. The escrow contract reads governance values at wager creation time and snapshots them, so fee changes never affect existing wagers retroactively.
Hardcoded fee caps in the escrow contract (e.g., protocol fee can never exceed 10%) provide guardrails even if governance is compromised.
2. On-Chain Reads Instead of Subgraph
Early in development, the frontend relied on The Graph subgraph for all wager state. This introduced a 5-30 second indexing lag that caused real problems. Users would deposit, but the UI would still show "waiting for deposit" because the subgraph hadn't caught up.
The fix was adding direct on-chain reads via eth_call to getWager(bytes32). The frontend now reads deposit status, wager state, and outcomes directly from the contract in real time. The subgraph is still used for listing wagers and historical queries, but any time current state matters (deposit confirmation, outcome submission, settlement), the frontend goes straight to the chain.
This dual approach gives us both speed (real-time state) and queryability (indexed history).
3. Manual ABI Encoding
Rather than importing ethers.js or viem as full dependencies, the frontend manually encodes function calls using hardcoded selectors and hex padding. For example, the deposit(bytes32) call is constructed by concatenating the selector 0xb214faa5 with the left-padded wager ID.
This keeps the frontend bundle small and avoids dependency on large libraries for what amounts to a handful of function calls. The trade-off is that any selector mistake silently breaks transactions, so all selectors were verified against ethers.Interface during development and are treated as constants.
4. ERC-4337 Smart Accounts with Passkeys
Traditional crypto wallets require users to manage seed phrases. This is a non-starter for mainstream users. BetBit uses ERC-4337 account abstraction to give each user a smart contract wallet controlled by a WebAuthn passkey (fingerprint, Face ID, or hardware key).
When a user signs up, the system registers a P-256 passkey with the browser's WebAuthn API and deploys a ZeroDev Kernel smart account that recognizes that passkey as the owner. All subsequent transactions are signed by tapping a fingerprint sensor rather than managing private keys.
The Pimlico paymaster sponsors gas for every UserOperation, so users never need to acquire or hold AVAX. Approve and deposit calls are batched into a single UserOperation, meaning one biometric prompt covers both the USDC approval and the escrow deposit.
5. Off-Chain Metadata with On-Chain Hashing
Wager descriptions, match details, and mediator contact information are stored off-chain in a relational database rather than on-chain. Storing text on Avalanche would cost significantly more in gas and isn't necessary for settlement.
To maintain integrity, the backend computes a keccak256 hash of the metadata and passes it to the createWager call. The smart contract stores this hash on-chain. If anyone later claims the metadata was tampered with, the hash can be verified against the on-chain record.
6. Blind Outcome Submission
When both parties submit their outcomes (WON, LOST, or DRAW), the contract stores them independently. The frontend hides the first party's submission from the second party until both have submitted or the deadline passes.
This prevents gaming. If Party A could see that Party B submitted "WON," they might just submit "WON" too (resulting in a draw refund) rather than honestly reporting "LOST." Blind submission keeps both parties honest.
Implementation Approach
The project is structured as a monorepo with five packages:
Development followed a bottom-up approach: contracts first, then backend API, then frontend. Each layer was tested independently before integration. The shared package ensures type consistency. The same WagerState enum and Wager interface are used in the contract tests, the backend API, and the frontend components.
The production deployment runs nginx as a reverse proxy, routing API requests to the Express backend and everything else to the Next.js frontend. The database runs on the same host. A process manager handles both Node.js services with automatic restarts, and SSL is provided via Let's Encrypt with automatic certificate renewal.
Last updated