Capstone
Overall Team Winner at Melbourne Institute of Technology's IMPACT 2025 capstone showcase, for a payment gateway built for a real client, Skill Sync, that gives every payment its own address.
How it was earned
The ambiguity
The client's brief was one line of goal: "a secure and universal payment gateway using blockchain", applicable to both client-server and peer-to-peer networks, integrated with PayPal or Stripe. Our first-semester plan answered it with everything: custom smart contracts in Solidity, crypto exchange, two-factor admin, 99.99% uptime and a dedicated wallet server.
source · client proposal · first-semester planWhat I cut: the part we couldn't take back
By the build semester two of five had left, and the plan was still sized for five. The riskiest item in it was the smart contracts. Once deployed, a contract can't be changed; it carries its own operational load; and none of us had the expertise to be sure we'd get it right the first time. Nothing in the client's need required one. So we dropped the contracts: every payment address is derived by our own backend from one wallet (an HD wallet), which is code we could still fix after shipping.
What I kept
What the system kept is the part that answers the client's real need: a hierarchical-deterministic wallet (BIP39/BIP32) that derives a fresh address for every payment, so each payment matches its order without a shared address. A backend I wrote in Web3.js owns the whole flow: generate the address, watch the chain through Infura, store the funds, and release them to the merchant with a signed transaction. Keys are stored encrypted with AES-256-CBC. Stripe sits alongside for card payments.
source · detailed design · gitShown early, not late
Vladimir organised the client meetings; I brought a working prototype to them and used it to settle requirements with the least back-and-forth. The first one went in front of the client in the first weeks of the build semester: a unique address with a QR code, funds arriving in the HD wallet, and the merchant releasing them. The client later wrote that "the unique address feature significantly reduced our transaction costs."
source · individual report · posterWhat broke: the wallet forgot where its own money was
Each payment address is derived from one secret at a numbered position (its index). The system saved each address with its index, and when funds had to be released it re-derived the key from that saved index. Where the saved index was wrong or missing (the code fell back to 0), it derived a different key, so it couldn't sign for money that was sitting at a real address. I wrote a repair that re-derives the first 21 positions from the secret, rebuilds the true address → index map and corrects the stored records. The release path now re-derives every stored address and checks it matches before trusting it, and it falls back across five network providers if one is down.
source · fix-derivation-indexes.js · recover-and-release.jsThe first amount check let wrong payments through
Pay the invoice
The demo’s order asked for 0.00181982 ETH. Send something else.
0.00182000 ETH+0.0099%
14 Apr · ±0.5%, compared in exact wei
Accepted
within 0.5% of the ask
10 May · six decimals, one unit of rounding
Accepted
matches to six decimals
Both rules accept it.
- SAWThe check accepted anything within 0.5%, so a payment short at the fourth decimal counted as paid.0.00025 ETHshort on a 0.05 ETH order
- SAWWhen it couldn’t read an amount, it counted the payment as correct.
- CHANGEDThe next day: anything the check can’t verify counts as wrong.fail closed
- CHANGEDThen the margin tightened: the amounts must match to six decimal places, allowing one unit of rounding and never more than 0.000002 ETH.6 decimals
- HOLDSThe demo asked 0.00181982 ETH; a wallet sent 0.00182. Accepted.0.00182 ETHdemo payment
- NOT YETNo automated tests, and it compares floating-point numbers, not integer wei.
A mismatched payment is recorded, and its address is retired and kept out of the funds the merchant can release. The customer is told the order won't be processed.
The demo shows why the margin sits at six decimals rather than exact. The checkout asked for 0.00181982 ETH, but a wallet can't easily send eight decimals, so the payer sent 0.00182. The gateway accepted it, and the merchant later released 0.001818 ETH after the network fee. An exact match would have rejected an honest payment; the old 0.5% margin would have accepted a short one.
source · demo video 00:46–01:30New payments never share an address with money already in it
Before assigning an address, the gateway scans ahead, up to 1,000 positions, for one with a zero balance, so every payment can be told apart on the chain.
source · paymentController.jsWhat stands
- A working gateway the judges and client used.
- Overall Team Winner at IMPACT 2025.
- Supervisor on the poster: "An innovative approach to secure Ethereum transactions with excellent implementation."
What it doesn't do
Stated in our own final presentation:
- no smart contracts, and no refunds;
- ETH only, on the Sepolia testnet;
- untested with wallets other than MetaMask;
- never security-audited.
And from the code:
- A wrong payment is refused, not refunded. The money stays at the retired address.
- The amount check has no automated tests, and it still compares floating-point numbers rather than exact integer wei. The first version, on 14 April, did compare exact wei; the fix that made it fail closed also moved it to floating point.
- Found on re-reading the code for this page: two code paths still choose the next address position in different ways, so the check before each release, not the creation step, is what protects the funds.
Sources
- The repository ↗
- History of the amount check (isPaymentAmountCorrect) ↗
- fix-derivation-indexes.js, and recover-and-release.js beside it ↗
- paymentController.js ↗
- Course documents: the client proposal, both semester plans, my individual report and the poster. Not public.