Proving a donation was not changed, with a team of 15
FundChain is a fundraising platform for BINUS students, built as a university project. I was the Product & Tech Lead: I set the product scope, wrote the spec, built the foundation, and ran four workstreams on top of it.
- 15
- People led
- 4
- Workstreams
- 6
- Sprints
- 99
- Jira tasks
- 27
- Planning docs
- 19
- Spec conflicts resolved
A donation record anyone with database access can edit
Student fundraisers ask people to trust a spreadsheet. If someone with database access edits a donation after it is paid, nobody outside can tell.
The brief was to make that edit visible. Keeping the money on-chain was out of scope: payments stay in rupiah through a normal gateway, and the chain is only a proof layer.
What we had to work with
Four decisions, and what each one cost
- 01
One canonical hash, defined in one place
Each donation marked paid becomes the string v1|donationId|pseudonym|amount|paidAtSeconds, hashed with Keccak-256. It lives in a shared package used by both the notarizer and the checker, so the two can never format it differently.
Trade-off. Fields are strict: whole rupiah only, a pseudonym like STU-00042 instead of a name, and the stored payment time instead of now(). The v1 prefix leaves room to change the format later.
- 02
A relayer that can only write once
The contract has two roles. A cold admin wallet can revoke the relayer; the hot relayer on the backend is the only one allowed to notarize, and a second write to the same donation reverts.
Trade-off. A wrong hash can never be corrected on-chain. That is the point, but it means the hash format had to be settled before the first notarization.
- 03
An outbox table instead of Redis and BullMQ
The plan called for Redis and BullMQ. Notarization jobs go into a blockchain_transactions table instead, and a worker polls it with retry, backoff, and a FAILED state.
Trade-off. Polling is slower than a queue, and on Vercel the worker only runs when a request or the daily cron wakes it. One less service to host for free and to explain to 15 people.
- 04
Freeze the campaign, not just flag the row
The integrity checker recomputes every hash from the database and compares it with the chain. A mismatch marks the donation TAMPERED, freezes the campaign, writes an audit log, and blocks every disbursement.
Trade-off. One bad row stops a whole campaign until an admin looks at it. For a fundraiser, a false stop is cheaper than a quiet payout.
From 27 docs to four workstreams
- Resolved the 19 spec conflicts in writing before any code: hash format, relayer role, webhook idempotency, and the rest.
- Built the monorepo myself (NestJS API, React web, Hardhat contract, Supabase) and had it live on Vercel and Sepolia by day two, so the team started from working code.
- Opened five epics in Jira: 91 tasks across four workstreams, each with one owner, and 8 integration tasks (CI, sprint checks, demo rehearsal) that I kept.
- Linked GitHub to Jira with BFD-key branch and PR names, and reviewed and merged the team's pull requests.
What runs today
- Web and API live on Vercel; donations notarized on Sepolia with an Etherscan link per donation
- A public proof page per donation that recomputes the hash in the browser
- The demo path runs end to end: pay, hash, notarize, edit the amount in the database, detect, freeze, refuse the payout
- Tests on the canonical hash, the contract (9 tests), the campaign state machine, the disbursement rules, and webhook signatures
Screenshots
What is not done yet
- Sign-in is a persona switcher for the demo. Microsoft SSO for BINUS is researched but not built.
- Payments run on a mock provider, with a Pakasir adapter ready. No real money has gone through it.
- On Vercel the automatic check runs once a day, so a tamper can sit unnoticed for hours unless an admin runs the check.
- It runs on Sepolia, a test network. Mainnet would need a funded relayer and a plan for gas.
The same story as a 3D walkthrough
Spec, foundation, plan, assign, review, ship, on the homepage.
