Bunch is a drop-in loyalty layer for Bitcoin-accepting merchants. It tracks punches locally alongside existing payment flows. Bunch never has custody of funds—all payments go through the merchant's payment infrastructure. Made for the btc++ Taipei hackathon.
- Vite + React + TypeScript for fast iteration
- Tailwind CSS for kiosk-friendly + mobile-first styling
- IndexedDB via
idbto persist merchant cards, sessions, and ledgers locally - BroadcastChannel for real-time sync between
/merchantand/customer - QR code generation + scanning with fallback paste mode
- Strict integrity checks
- Single-use purchase nonces with 10-minute expiry
- Duplicate scans rejected client-side
- Session end clears all volatile data
- Demo mode toggle keeps judges on the happy path: Mark Paid always succeeds, no external services
- Merchants define a single punch card template per device
- Each customer receives a random, per-merchant ID stored locally; no accounts, emails, or phone numbers
- No custody: Bunch never holds funds—all payments go to the merchant's wallet
- Redemption requires explicit merchant confirmation
- Copy emphasizes boundaries: "Bunch does not handle payments. Bunch tracks rewards after payment."
Bunch is designed to work alongside existing Bitcoin payment infrastructure like BTCPay Server, LNbits, or any Lightning/Bitcoin payment system. Bunch never has custody of funds—all payments go directly to the merchant's wallet.
How It Works:
- Merchant generates purchase QR → Bunch creates a unique purchase nonce (single-use, 10-minute expiry)
- Customer scans QR → Customer's device claims the nonce and waits for confirmation
- Payment happens → Merchant processes payment via their existing POS system (BTCPay, LNbits, etc.)
- Merchant confirms payment → Merchant marks the purchase as paid in Bunch, awarding the punch
- Customer sees update → Real-time sync updates customer's progress
Key Points:
- ✅ Works with any payment system
- ✅ Merchant has full control
- ✅ No API keys or configuration needed
- ✅ Perfect for merchants who want to verify each payment manually
When enabled, Bunch can automatically create invoices in your BTCPay Server and track payment status. This is completely optional and requires explicit merchant configuration.
How It Works:
- Merchant configures BTCPay → Enters BTCPay Server URL, API key, and Store ID
- Merchant generates purchase QR → Bunch creates purchase nonce AND automatically creates invoice in merchant's BTCPay Server
- Customer scans QR → Customer's device claims the nonce
- Customer pays → Payment goes directly to merchant's BTCPay Server wallet (Bunch never touches funds)
- Bunch tracks payment → Polls BTCPay Server every 5 seconds to check invoice status
- Auto-award punch → When invoice is paid, Bunch automatically awards the punch
Key Points:
- ✅ Still no custody: All funds go to merchant's BTCPay Server wallet
- ✅ Merchant controls everything: Invoice created in merchant's BTCPay Server using merchant's API key
- ✅ Optional convenience: Reduces manual "mark paid" steps
- ✅ Can be disabled: Switch to Demo Mode or clear BTCPay config to use manual mode
- ✅ Manual override always available: Merchant can still manually mark purchases as paid
Important Distinction:
- Bunch can create invoices in the merchant's BTCPay Server (optional automation)
- Bunch never has custody of funds (all payments go to merchant's wallet)
- Bunch only reads payment status (no ability to move funds)
- This is merchant's infrastructure (merchant owns BTCPay Server and all funds)
When a payment is confirmed in BTCPay Server or LNbits, send a webhook to your backend that notifies Bunch:
// Backend webhook handler (Node.js example)
app.post('/webhooks/btcpay', async (req, res) => {
const { invoiceId, status, metadata } = req.body
if (status === 'paid' && metadata.purchaseNonce) {
// Notify merchant's Bunch instance via WebSocket or Server-Sent Events
notifyMerchant({
type: 'payment-confirmed',
purchaseNonce: metadata.purchaseNonce,
invoiceId,
amount: req.body.amount
})
}
res.status(200).send('OK')
})BTCPay Server Setup:
- Create invoice with metadata:
{ purchaseNonce: "abc123..." } - Configure webhook URL in BTCPay Server settings
- Webhook fires on
invoice.paidevent - Backend forwards confirmation to merchant's Bunch instance
LNbits Setup:
- Create payment request with metadata:
{ purchaseNonce: "abc123..." } - Configure webhook in LNbits extension settings
- Webhook fires on payment success
- Backend forwards confirmation to merchant's Bunch instance
Merchant's Bunch instance polls payment system API to check invoice status:
// In merchant store (useMerchantStore.ts)
const checkPaymentStatus = async (purchaseNonce: string) => {
// Get invoice ID from purchase nonce metadata
const invoiceId = await getInvoiceIdForNonce(purchaseNonce)
// Poll BTCPay Server API
const invoice = await fetch(`https://your-btcpay-server.com/api/invoices/${invoiceId}`, {
headers: { 'Authorization': `token ${BTCPAY_API_KEY}` }
}).then(r => r.json())
if (invoice.status === 'paid') {
await markPaid(purchaseNonce, { amount: invoice.amount })
}
}Merchant manually confirms payment after verifying in their POS system. This is what demo mode uses, and works perfectly for small merchants who want to verify each payment manually.
- Invoice Creation: Include
purchaseNoncein invoice metadata when creating payment - Webhook Endpoint: Set up webhook receiver (or use polling)
- Payment Verification: Verify payment amount meets card's
minSatsrequirement - Nonce Matching: Match webhook's
purchaseNonceto pending purchase in Bunch - Error Handling: Handle expired nonces, duplicate payments, and failed verifications
- UI Updates: Ensure merchant and customer UIs update in real-time on confirmation
// 1. Create invoice with purchase nonce in metadata
const invoice = await btcpay.createInvoice({
amount: 10000, // sats
currency: 'BTC',
metadata: {
purchaseNonce: purchase.nonce,
cardId: card.id,
sessionId: session.id
}
})
// 2. Webhook receives payment confirmation
btcpay.on('invoice.paid', async (invoice) => {
const { purchaseNonce } = invoice.metadata
// 3. Verify and award punch
if (await verifyPurchaseNonce(purchaseNonce)) {
await merchantStore.markPaid(purchaseNonce, {
amount: invoice.amount,
invoiceId: invoice.id
})
}
})- Nonce Expiry: Purchase nonces expire after 10 minutes to prevent replay attacks
- Single-Use: Each nonce can only be claimed once
- Amount Verification: Always verify payment amount meets
minSatsrequirement - Merchant Confirmation: Merchant must explicitly confirm payment (prevents auto-awarding on wrong invoices)
- Session Isolation: Purchases are tied to specific sessions, preventing cross-session fraud
Core Principle: No Custody
- Bunch never holds funds—all payments go directly to the merchant's wallet
- Bunch never processes payments—merchants use their own payment infrastructure
- Bunch only tracks loyalty rewards after payment confirmation
Optional Automation:
- BTCPay integration is optional—merchants can use manual mode (default)
- When enabled, Bunch creates invoices in merchant's BTCPay Server (merchant's infrastructure)
- Merchant maintains full control—they own BTCPay Server, API keys, and all funds
- Bunch only has read access to payment status (cannot move funds)
Benefits:
- Works with any Bitcoin payment system (BTCPay, LNbits, custom solutions)
- Reduces attack surface (no payment handling = less risk)
- Merchant maintains full control over payment flow
- Bunch focuses solely on loyalty tracking, not payment processing
- Allow customers to gift a completed reward (e.g. free coffee) to another
npub - Merchant redeems one free item as usual, but the reward can be claimed by someone else
- Enables social discovery moments like "Coffee Bunch / Brunch Bunch"
- Opt-in, post-reward sharing only—no Nostr posts in the core MVP
- Optional WebRTC transport fallback if BroadcastChannel unsupported
- Export/import punch history snapshot for migrating kiosks
- Merchant analytics overlay (most popular rewards, streaks)
- Production-ready webhook handlers for BTCPay Server and LNbits
- No user accounts or login
- No email / phone collection
- No Stripe or fiat rails
- No wallet custody (Bunch never holds funds—all payments go to merchant's wallet)
- No Nostr publishing in the MVP
- No backend servers—everything runs in the browser
Note on Invoice Creation:
- Bunch can optionally create invoices in merchant's BTCPay Server (requires merchant configuration)
- This is merchant's infrastructure—merchant owns BTCPay Server and all funds
- Bunch never has custody—it only reads payment status to automate punch awards
- Manual mode (no invoice creation) is always available and is the default