Filament Web3 Airdrop Platform
Token Distribution & Delegate Voting on an L2 Rollup
Role: Senior full-stack engineer in a team of 3–5: the Node server routes, the integration with the team's L2 rollup API, MetaMask signing of rollup transactions through a WASM serializer, the Ethereum wallet flows, and the campaign and voting web client. The rollup itself was built by teammates.
The campaign and voting web client: a multi-step airdrop campaign wizard, campaign pages with criteria and distribution vote charts, and Ethereum wallet flows with ethers.js and MetaMask behind one transaction adapter. ethers.js and the charting code are split into separate chunks, and the WASM signer is lazy-loaded only when a user signs.
Key Features
- Node server routes aggregating a blockchain-analytics API with per-widget graceful degradation (Promise.allSettled)
- Integration with the team's L2 rollup REST API: campaign, vote and account queries, sequencer submission, ledger status
- MetaMask signing of rollup transactions via the rollup's serializer compiled to WASM (EIP-191), lazy-loaded on first sign
- BigInt-safe payloads (json-bigint) for 64-bit ids, nonces and vote weights; nonce 0 for wallets new to the rollup
- Ethereum (Sepolia) wallet flows with ethers.js: connect, account and chain changes, network switching, delegate-registry reads
- Campaign wizard, delegate voting and campaign pages; Clerk auth with PostgreSQL (Supabase)
Tech Stack
Frontend & Tooling
Blockchain
Backend & APIs
Challenges & Solutions
Signing Rollup Transactions With MetaMask
The rollup encodes transactions in its own binary format, but users only have MetaMask. Re-implementing that serialization in TypeScript would drift from the rollup's types with every protocol change.
Used the rollup's own serializer compiled to WebAssembly and published as an npm package with generated types. The client fetches the nonce, serializes the call in WASM, signs it with EIP-191 personal_sign and submits the signed transaction to the sequencer. The WASM module is lazy-loaded only when a user signs.
Silent Precision Loss Between the Rollup and JavaScript
Campaign ids, nonces and vote weights are 64-bit integers on the rollup side. JSON.stringify and JavaScript numbers round anything above 2^53 without an error.
Serialized call payloads with json-bigint and passed ids and chain ids as BigInt end to end, so large values reach the signer exactly as the rollup expects them.
The First Transaction From a New Wallet
A brand-new Ethereum address has no account on the rollup yet, so the account lookup returned 404 and the client could not build the user's first transaction.
Added a per-call error hook to the shared fetch wrapper that maps that 404 to nonce 0. Nonce plus chain id in the signed payload keep replay protection intact.
Dashboard Aggregation That Degrades Gracefully
The analytics dashboard needed several upstream calls per campaign to an external blockchain-analytics API; one slow or failing call used to break the whole page.
A Node route loads the organisation's campaigns, then fetches aggregations, interactions and daily change for each in parallel with Promise.allSettled at both levels. A failure nulls only that widget, and the route also returns status counts for the header.