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.

Product

Two-sided app · customer + merchant

Wallet

Firestore atomic ledger

Payments

Stripe top-ups + payouts

Transfer

QR scan → instant credit

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.

QR client → Firebase → Stripe

Customer wallet + QR

qrcode.react

scan

Merchant scanner

@zxing/browser

credit

Round-up amount

entered at checkout

Firebase SDK · verified ID token

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.

Scan → Credit → Fund → Cash out

01

Scan

At checkout the merchant opens the in-app scanner (@zxing/browser) and reads the customer’s QR code, which encodes their wallet UID.

02

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.

03

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.

04

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.

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.

Cloud Functions

createStripeCustomer

Creates a Stripe customer record for a merchant.

createSetupIntent

Starts securely saving a merchant’s card on file.

savePaymentMethod

Attaches the saved card as the default method.

topUpWallet

Charges the saved card (paymentIntents) to fund the float, currency by country.

handleUserWithdraw

Creates the Stripe payout / checkout for a customer cash-out.

emailOnWithdraw

Firestore onDocumentCreated trigger that emails the withdrawal confirmation.

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.

Tech stack

React 19ViteReact RouterTailwind CSSFramer MotionFirebase AuthFirestoreCloud FunctionsFirebase HostingStripe@zxing/browserqrcode.reactdate-fns