Case Study · Documentation
CoinUp.
A payment platform that turns spare change into a digital wallet. A merchant credits a customer's round-up over a QR scan; the customer then donates, invests, or withdraws it. Over 100 users onboarded in a live pilot.
At a glance
Product
Two-sided app · customer + merchant
Wallet
Firestore atomic ledger
Payments
Stripe top-ups + payouts
Transfer
QR scan → instant credit
Overview
Spare change, made spendable
Cash change is friction - it ends up in a jar or lost entirely. CoinUp captures it digitally instead. When a customer pays, the merchant credits the round-up straight to the customer's CoinUp wallet with a single QR scan, no card reader or extra hardware required.
It's really two apps sharing one codebase and one backend. Customers hold a wallet and decide what their accumulated change becomes - a donation, an investment, or a withdrawal. Merchants scan, credit, and fund a float. Everything settles through Firebase and Stripe, with the money-moving logic wrapped in atomic database transactions.
Architecture
QR client → Firebase → Stripe
Client · React SPA (Vite + Tailwind)
Customer wallet + QR
qrcode.react
Merchant scanner
@zxing/browser
Round-up amount
entered at checkout
Backend · Firebase (project coinup2025)
Cloud Functions
secrets server-side
Stripe
payments + payouts
Firestore
atomic wallet ledger
Hosting
coinup.ca
The merchant is always the signed-in user; the customer's UID rides in the scanned QR. Balance writes are atomic, so a credit can never race itself.
Money flow
Scan → Credit → Fund → Cash out
Scan
At checkout the merchant opens the in-app scanner (@zxing/browser) and reads the customer’s QR code, which encodes their wallet UID.
Credit
The merchant enters the spare-change amount. creditChange runs a Firestore transaction that atomically increments the customer’s balance and writes both a customer and a merchant transaction log.
Fund
Merchant float and top-ups run through Stripe via Cloud Functions - setupIntents to save a card, paymentIntents to charge it - with the currency chosen by country.
Cash out
Customers donate to a charity, invest, or withdraw. A withdrawal triggers a Stripe payout Cloud Function and an emailOnWithdraw Firestore trigger sends the confirmation.
Product
Two apps, one codebase
Customer
A wallet balance, a personal QR code to receive spare change at checkout, and three things to do with it - donate to a charity, invest, or withdraw to a bank account.
Merchant
Scan a customer to credit their change, top up the float with a saved card, and review a running history of every transaction the store has processed.
Backend
Cloud Functions
createStripeCustomerCreates a Stripe customer record for a merchant.
createSetupIntentStarts securely saving a merchant’s card on file.
savePaymentMethodAttaches the saved card as the default method.
topUpWalletCharges the saved card (paymentIntents) to fund the float, currency by country.
handleUserWithdrawCreates the Stripe payout / checkout for a customer cash-out.
emailOnWithdrawFirestore onDocumentCreated trigger that emails the withdrawal confirmation.
Guardrails
Design decisions
Atomic ledger
Every balance change happens inside a Firestore runTransaction, so a scan can never double-credit or lose money under concurrent writes. The customer and merchant logs are written in the same atomic step.
The QR is the rail
No card readers, no NFC - any phone camera is the terminal. The customer’s identity travels in the QR code; the merchant is simply the signed-in user, so their UID is never spoofable from the client.
Secrets stay server-side
The Stripe secret key lives only in Cloud Functions. The client holds nothing but the publishable key and a short-lived Firebase ID token, which every sensitive function verifies.
Currency aware
ip2location resolves the charge currency by country at top-up time, so the same codebase handles wallets funded in different regions without hard-coding a single currency.
Built with