Tip any Nostr creator in USDC on Solana — no backend, no custody.
Inspiration Nostr proved something important: social data works better when it's open. Notes live on relays, clients compete, and nobody owns the graph. But Nostr's native tipping — zaps — runs on Bitcoin Lightning, whic
Inspiration
Nostr proved something important: social data works better when it's open. Notes live on relays, clients compete, and nobody owns the graph. But Nostr's native tipping — zaps — runs on Bitcoin Lightning, which is hard to get if you live somewhere like Dakar, where I'm building from. Getting Lightning liquidity, channels and custody right is a real barrier.
Meanwhile, stablecoins on Solana have quietly become the practical dollar rail for a lot of people: fees are a fraction of a cent, settlement is instant, and cashing out through existing local rails is realistic.
So I asked a simple question: what if zaps were stablecoin tips on Solana, and the zap receipt was just another open Nostr event? That's Kola. I built it solo, in 48 hours, for the MLH × Solana Hackathon.
What it does
Kola lets anyone tip any Nostr creator in USDC:
A creator opts in once. They publish their Solana address on their Nostr profile (kind 0 metadata: solana_address + solana_network) using Kola's Creator setup tab. Nostr already has the audience; now it has the payout address too.
Anyone can zap them. In the Zap someone tab, paste the creator's npub, NIP-05 or hex key, choose a USDC amount, and pay with a browser wallet or by scanning the Solana Pay QR with any mobile wallet.
The payment verifies itself. Every zap embeds a fresh one-time reference keypair. Kola's browser code watches the public Solana RPC for a transaction containing that reference; the moment it appears, the payment is confirmed on-chain and parsed (recipient + exact USDC amount).
The zap becomes open data. Kola publishes a kind 9735 zap receipt back to Nostr relays with tags for the recipient, the amount, the Solana signature and the network. The tip shows up in Kola's live Zap feed — and in any Nostr client that renders zap receipts.
There is no server, no database, no custody and no private keys held anywhere. The chain is the backend; the relays are the social layer.
Protocol (MVP):
Artifact Convention
Creator opt-in kind 0 profile: solana_address + solana_network (devnet | mainnet-beta)
Payment USDC SPL transferChecked (6 decimals) with a one-time reference key, Solana Pay URL + QR
Zap receipt kind 9735 event: tags p (recipient), amount (USDC micro-units), tx (Solana signature), network
USDC mints
How we built it
Frontend: SolidJS + Vite + TypeScript (strict), Tailwind CSS 4, linted with oxlint. A light-blue/white interface with three tabs: Zap someone, Creator setup, Zap feed.
Payments: @solana/pay, @solana/web3.js and @solana/spl-token. Each zap is a USDC transferChecked with a one-time reference key and a kola memo, wrapped in a Solana Pay URL and QR.
Serverless verification: the browser generates a fresh keypair per zap, embeds the public key as the Solana Pay reference, and polls getSignaturesForAddress(reference) on the public RPC. When the transaction shows up, it is parsed and confirmed with the exact micro-USDC amount. No webhooks, no listener service.
Nostr: @nostr-dev-kit/ndk for profiles, publishing and a live kind-9735 subscription, across public relays (damus, nos.lol, primal, nostr.band).
Quality gates: Vitest unit tests for the money-formatting logic, a GitHub Actions CI workflow, and a custom end-to-end harness (bun run e2e) that runs the whole zap flow — build, sign, confirm, verify by reference — currently passing 13/13 checks against a Solana test validator.
Challenges we ran into
The bug that ate a day: "Failed to simulate this request" in Phantom. Every transaction Kola built failed simulation in the wallet, which only offered "Confirm (unsafe)". Root cause: I attached the one-time reference key to the SPL Memo instruction as a non-signer account — but the memo program requires every provided account to be a transaction signer, so the transaction was born invalid. The fix (matching the Solana Pay spec and @solana/pay itself) was to ride the reference key on the transferChecked instruction as a read-only non-signer account. getSignaturesForAddress still finds it, and simulation now passes.
Amount precision. USDC has 6 decimals and "0.5 USDC" becomes 500000 micro-units; floating-point math has no place near that. All formatting lives in pure, unit-tested helpers using BigNumber.
Recipients who never held USDC. The creator's associated token account may not exist yet, so the sender's transaction must bootstrap it (and pay its rent). Kola pre-flights the wallet's SOL balance for fees plus ATA rent and blocks the zap with faucet guidance instead of letting Phantom throw an opaque error.
Devnet is a harsh roommate. The faucet was closed/captcha-gated during the hackathon, so I built an E2E harness on solana-test-validator to prove the flow locally: happy path simulates, confirms and verifies by reference with the exact amount; no-SOL, overspend and stale-blockhash cases are all caught before the wallet even opens.
Accomplishments that we're proud of
Genuinely serverless payment verification. Solving "who confirms the payment happened?" with zero infrastructure — one-time reference keys + public RPC polling — and having it work in code and in E2E.
A real protocol spec, not just an app: kind 0 Solana fields, kind 9735 receipt tags, verified mints per cluster — documented in the README so others can implement it.
Engineering hygiene under deadline pressure: strict TypeScript, oxlint clean, unit tests, CI, 13/13 E2E checks, MIT license, honest documentation.
A polished product, solo, in 48 hours — logo, design system, three flows, from zero.
What we learned
The Solana Pay spec has teeth: instruction account semantics (signer vs non-signer, read-only vs writable) decide whether a transaction simulates, and transferChecked exists for a reason.
Wallet UX is where crypto apps go to die. Translating error code 4001, simulation failures and 0x1 (insufficient USDC) into actionable sentences changed the whole feel of the app.
Building with no backend forces better design: if the chain plus relays can't do it, you have to justify adding anything else.
What's next for Kola
Close the final mile on live wallets (see the honest note below — this is priority zero).
Cryptographic ownership proofs for solana_address (a signed Solana message referenced from the profile), so a compromised profile can't redirect tips.
Solana Blinks: zap directly from inside Nostr clients without opening the app.
Multi-token support (USDT, local stablecoins), low-amount presets, and a mainnet launch tuned for African creator economies.
Built with
solidjs, vite, typescript, tailwindcss, oxlint, vitest, nostr, ndk, solana, solana-pay, solana-web3js, spl-token, usdc, qrcode, phantom, github-actions
Honest status
Full transparency: the MVP is not working end-to-end yet. Everything you see above is real and in the repo — the UI, the Solana Pay request building, the reference-key verification, the kind 9735 publishing, and a 13/13-passing end-to-end run of the whole zap flow against a Solana test validator. But on my last live test with a real Phantom wallet on devnet, the final transaction still failed to complete, and I ran out of hackathon before I could close that last gap.
This was my first hackathon. I'm submitting anyway, because the protocol is real, the code is honest, and I learned more in these 48 hours than in months of tutorials. I will definitely make it work next time — and I'd genuinely welcome any feedback on the wallet-signing step from teams who've shipped Solana Pay integrations.
GitHub repo: https://github.com/ibousv/kola
Live demo: https://kola-eight.vercel.app/
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.