Lamport - From a Solana Workshop to a Neo-Bank Demo on Rails

Lamport - From a Solana Workshop to a Neo-Bank Demo on Rails

A few days ago I attended a Solana workshop in Thailand, run by Superteam Thailand and led by Gui Bibeau, Product Engineering Lead at the Solana Foundation. The topic: the Solana Developer Platform (SDP) — an open-source, API-first platform that bundles the building blocks of a financial product: custody, wallets, payments, RPC, compliance, fiat rails. One consistent API layer, providers you can mix and match underneath.

The live demo was institutional-grade — a permissioned Money Market Fund token with allow lists, transfer restrictions, and pausability, all orchestrated through APIs. Cool, but what stuck with me was the simpler promise underneath. Gui posted it afterwards as a playbook:

Fork it today. Add your env keys. Host it. Build your unique product on top. Things I’d build with it: Tokenization SaaS, Neo bank, Money market fund, RWA engine.

I build Solana tooling for Rails — SolRengine — so “build on top of it” was an easy sell, and the neo-bank sounded like the most fun item on the list. A few evenings later: Lamport, a small neo-bank demo in Rails 8 on a self-hosted SDP. Sign up and a real custody wallet is provisioned for you on devnet. Add SOL, send money to other users with an on-chain proof link, set up recurring payments that fire real transfers, and watch balances update live when money arrives.

One rule shaped the whole thing: no internal ledger. Every balance on screen is a real on-chain balance, read through SDP. There’s no bookkeeping table pretending to be money.

This post is what I learned walking the playbook.

The Stack

Layer Choice Why
Framework Rails 8.1 + Hotwire Boring on purpose
Database SQLite ×3 Zero infra
Payments SDP, self-hosted (v0.27) The thing being tested
Custody Privy, via SDP Per-user wallets
Fees Kora (local docker) Sponsored transactions
Realtime solrengine-realtime + Solid Cable Doorbell → Turbo Streams
Prices solrengine-tokens (Jupiter) SOL → USD
CSS / JS One CSS file, importmap No build step

The app holds no keys and signs nothing. Every wallet, balance, and transfer goes through SDP’s REST API with a server-side key.

What’s in the Demo

Where the Playbook Got Bumpy

SDP is v0.27, pre-mainnet, and shipping daily — gaps are expected, and everything below is simply what I found this week. The real question is whether you can build around them. You can. What I hit:

One wallet per project on local custody. The zero-vendor local provider holds exactly one root wallet — and a neo-bank needs a wallet per user. Solution: a managed custody provider. Privy’s free tier worked with the app unchanged — same SDP endpoints, just provider: "privy" when creating wallets.

Transfers go through Kora. The minimal config can’t execute a transfer on its own — the native fee adapter deliberately doesn’t submit transactions (the code says it would break the platform’s port abstraction), so Kora is the supported path. Running it locally is one docker-compose plus a funded devnet fee payer, and as a bonus your users’ transactions are gas-sponsored.

No push channel for wallet activity. No webhooks, no event streams. For live screens you either poll or watch the chain yourself. I watched the chain — next section.

No USD prices on balances. The schema has a usdValue field, but the payments balances endpoint never fills it. The “≈ $” line under the balance comes from Jupiter via solrengine-tokens instead, cached for 60 seconds and labeled indicative — it’s a mainnet price on a devnet balance, and an honest demo says so.

I’m keeping a friction log of all of these to feed back upstream — that’s the build-in-public part.

The Doorbell Pattern

The live screens work like this: a small watcher process holds one accountSubscribe WebSocket per user wallet, via solrengine-realtime. When an account changes on-chain, the notification is treated as a doorbell — it carries no data, it just means something changed. The app re-fetches everything from SDP and pushes updates to the browser over Turbo Streams and Solid Cable:

# doorbell rings → re-fetch from SDP → broadcast (or stay silent)
def attempt_broadcast(user)
  sol = Sdp::Sol.read_balance(user.sdp_wallet_id)
  return false if sol.nil?

  entries = ActivityFeed.entries_for(user)
  broadcast(user, sol, entries)
  true
rescue Sdp::Error
  false
end

Two rules make it feel right:

The result: screens change only when money actually moves, the chain stays a trigger rather than a data source, and SDP remains the single source of truth.

Small Details, Big Difference

Two touches that made the money flows trustworthy:

Every manual send carries a unique memo. If a transfer request times out, the recovery logic looks for that exact memo in the transfer history — positive identification instead of guessing by recipient and amount. The receipt only ever says “confirmed” when it provably is.

“No money moved” is only said when it’s provably true. The error handling distinguishes a request that never left the machine from one that was sent and went silent — the first gets a clear retry message, the second gets verified against the history before the app says anything.

Was Gui Right?

Mostly, yes. Lamport went from rails new to a working demo — per-user wallets, transfers with on-chain proof, recurring payments, live balances — without the app ever touching a keypair. Coming from building SolRengine, where I deal with the chain directly, the appeal here is the shape: SDP absorbs custody, signing, and fee handling, and the app stays a plain Rails app talking REST.

“Fork it and add keys” held up as the setup story. I kept notes on the rough edges along the way, and the useful ones will hopefully make their way back upstream.

The code is on GitHub: solana-rails-neobank

Jose

Written by Jose

Full-stack developer with 20 years of Rails experience, currently building on Solana.

Follow me on X
Back to blog