Rotating savings circles where nobody holds the money.
Each round, every member pays that round's recipient directly, in USDC on Stellar. Nothing is ever pooled, so there is no custodian, no float and no pot to steal. Round status is read back from the chain, so "did she pay?" is a fact anyone can check rather than a claim in a notebook.
Live app · How it works · Pollar SDK docs · Source · Explorer
The name blends two names for the same institution: susu (West Africa) and pasanaku (the Andes).
A savings circle concentrates everything into one person — organiser, collector, treasurer, record-keeper. That is a single point of failure as well as a single point of trust: when they disappear, whoever's turn it was loses everything and the circle dies with them.
The digital tools that exist have mostly rebuilt the notebook (due dates, reminders, reports) while the cash still lands in someone's hands. Susunaku changes the movement itself:
| Traditional circle | Susunaku | |
|---|---|---|
| Who holds the money | The organiser | Nobody |
| Record of payment | A notebook | The chain |
| A member abroad | Locked out | In the same circle |
| If the organiser leaves | Savings gone | Nothing to take |
| Unit of account | Local currency that drifts | USDC |
- Contribute — every member except the round's recipient pays that recipient, wallet to wallet.
- Settle — USDC on Stellar, ~5 seconds, a hundredth of a cent in fees.
- Verify — status is derived from Horizon, with a transaction hash per contribution.
Members sign in with an email code. Pollar creates a non-custodial Stellar wallet, pays the account's reserves and opens its USDC trustline, so nobody needs a seed phrase or a browser extension. A Stellar payment's fee is charged to the sender's own XLM — see Fees, honestly.
Requires Node 20+.
npm install
cp .env.example .env.local # optional: bake in your key and network
npm run dev # http://localhost:5187The app needs HTTPS or localhost: Pollar's auth uses WebCrypto/DPoP, which browsers only expose in a secure context.
Create an app at https://dashboard.pollar.xyz → Build → API Keys and copy the
publishable key (pub_testnet_…). Testnet keys are issued immediately. Either paste it into
the app once (it is remembered on that device), or set it at build time:
VITE_POLLAR_PUBLISHABLE_KEY=pub_testnet_xxx
VITE_STELLAR_NETWORK=testnetAdd your origin (e.g. http://localhost:5187 and your production domain) to
Build → Domains in the dashboard, or the SDK will refuse the requests.
Never put a sec_… key in this repo — secret keys belong on a backend. A pat_… personal access
token is also not usable here: Pollar's client endpoints answer API_KEY_TYPE_NOT_ALLOWED.
A circle needs at least two members, and a member has to be able to receive USDC, which on Stellar means the account needs a trustline — a bare Friendbot account cannot be one:
npm run member:new -- Ama # funded account with the USDC trustline openFor USDC on testnet use the Circle faucet, or fund the wallet Pollar creates for you once you sign in. If a wallet has no XLM it cannot submit a payment:
npm run fund:gas -- G...ADDRESS 5New circle → amount, cadence, members and their addresses → Create. Copy the invite link: the entire circle (amount, cadence, members, rotation order) travels inside the link, so the other members just open it. Nothing is registered on a server for a circle to exist.
Three separate things, and only two are free:
- Wallet reserves and the USDC trustline — sponsored by Pollar. A member needs no XLM to hold USDC.
- A payment's network fee — charged in XLM to the sending account. Pollar's transaction options
for a Stellar payment are
{ timeoutSec, memo, maxFeeStroops }: there is no fee-sponsorship option in the SDK, and the client refuses to build a payment from a wallet holding zero XLM. The operator-side fix is Dashboard → Treasury → Sponsorship (tickpayment) with a funded gas wallet, plus Account Funding so new wallets start with spendable XLM. - Cost per payment — measured on testnet: 0.00001 XLM per contribution.
The app warns before the click when a contributor's wallet has no XLM, rather than failing after it.
Real money needs four things, in this order:
- Identity verification with Pollar. Their model verifies the person, not a corporation — testnet is instant, mainnet is an automated personal check.
- A
pub_mainnet_key for your app. VITE_STELLAR_NETWORK=mainnetat build time (the USDC issuer switches to Circle's mainnet asset automatically — seesrc/lib/config.ts).- Members funded with real USDC.
The UI refuses a key whose network disagrees with the build, and shows a red warning on mainnet because contributions are irreversible.
Live at https://susunaku.sithunyein.com — Vercel project susunaku, source at
github.com/thesithunyein/susunaku.
The build is a static bundle, so any static host works.
Vercel (what production uses)
vercel link --project susunaku # first time only
vercel --prodThe domain is already attached to the project and sithunyein.com is on Vercel's own nameservers,
so DNS and TLS are automatic — every vercel --prod republishes the live URL.
Connecting the key on production:
vercel env add VITE_POLLAR_PUBLISHABLE_KEY production # pub_testnet_... or pub_mainnet_...
vercel env add VITE_STELLAR_NETWORK production # testnet | mainnet
vercel --prodStatic hosts that read a CNAME file (GitHub Pages, Cloudflare Pages) already have
public/CNAME set to susunaku.sithunyein.com.
flowchart TB
Member[Member · email code, no seed phrase] --> App[Susunaku app]
App --> Pollar[Pollar SDK · @pollar/core]
Pollar --> Wallet[Non-custodial wallet · sponsored reserve + USDC trustline]
App --> Pay["runTx(payment) · N−1 direct USDC transfers"]
Pay --> Chain[Stellar ledger]
Chain --> Horizon[Horizon API]
Horizon --> Status[useRoundStatus · who has paid, from the chain]
App --> Link[Invite link · the whole circle travels in the URL]
App --> Corridor[Corridor panel · ramps in and out]
Status --> Receipts[Explorer receipts · one tx hash per contribution]
Nothing in that diagram is a server we run: the app is a static bundle, the wallet is the member's own, the record is the ledger, and the directory is the link itself.
Every file in the repository, what it does, and its size. No hidden directories, no "and some glue": the app is small enough to read in an afternoon, on purpose.
susunaku/
├─ index.html 25 shell, meta/OG tags, favicon, theme colour
├─ vite.config.ts 14 Vite + React plugin, dev-server allow-list
├─ tsconfig.json 20 strict TypeScript, bundler resolution
├─ vercel.json 8 SPA rewrites for the hash router
├─ package.json 30 scripts; deps: react, @pollar/core, stellar-sdk
├─ .env.example 8 VITE_POLLAR_PUBLISHABLE_KEY, VITE_STELLAR_NETWORK
│
├─ src/
│ ├─ main.tsx 19 root: PollarGateway → LangProvider → App
│ ├─ App.tsx 222 hash router, top bar, menu, countdown pill, auth sheet
│ ├─ gaze-frames.json 72 70-row calibration: gaze angle → video frame time
│ ├─ data/
│ │ └─ names.ts 31 18 regional names for the same institution
│ ├─ lib/
│ │ ├─ pollar.tsx 718 the @pollar/core gateway: auth, wallet, balance, ramps, pay
│ │ ├─ i18n.tsx 206 en/es/pt dictionary, context, persistence, detection
│ │ ├─ corridor.ts 154 Africa ⇄ LatAm legs: live vs designed, honestly labelled
│ │ ├─ circle.ts 145 the model: cadences, round maths, rotation, velocity
│ │ ├─ stellar.ts 145 Horizon lookups; matches a payment to its round window
│ │ ├─ useRoundStatus.ts 112 who has paid, derived from the chain — never stored
│ │ ├─ store.ts 74 local circle store (localStorage + invite links)
│ │ ├─ config.ts 71 network, USDC issuers, Horizon + explorer URLs
│ │ ├─ format.ts 58 countdowns, amounts, addresses
│ │ └─ share.ts 47 the whole circle encoded into one URL
│ ├─ components/
│ │ ├─ AuthPanel.tsx 318 sign-in, email-code flow, wallet card
│ │ ├─ CorridorPanel.tsx 314 money in / money out, from the SDK's own ramp list
│ │ ├─ icons.tsx 264 inline SVG set incl. official Google + GitHub marks
│ │ ├─ chrome.tsx 170 top bar, menu, footer, language switcher
│ │ ├─ HeroToy.tsx 142 the mascot: cursor-scrub video, mobile loop
│ │ ├─ art.tsx 81 the mascot as static mark + lockup
│ │ └─ Disclosure.tsx 46 collapsed sections — answer first, detail on request
│ ├─ pages/
│ │ ├─ Home.tsx 628 the round: stats, who-has-paid, receipts, invite, pay
│ │ ├─ CreateCircle.tsx 336 amount, cadence, members, rotation order
│ │ ├─ Info.tsx 172 How it works + Network & key
│ │ └─ JoinCircle.tsx 52 arriving from an invite link
│ └─ styles/
│ ├─ app.css 1287 the flat editorial system, every component
│ └─ tokens.css 116 colour, type, radius, spacing variables
│
├─ public/
│ ├─ favicon.svg 12 the mascot on the cream tile
│ ├─ mark.svg 25 the mascot, standalone
│ ├─ logo.svg 29 horizontal lockup with wordmark
│ ├─ toy-scrub.mp4 4.2MB 169-frame all-intra encode, seekable per frame
│ ├─ fonts/ Epilogue-Black.woff2, DMSans-Variable.woff2 (self-hosted)
│ └─ CNAME susunaku.sithunyein.com
│
├─ scripts/
│ ├─ check-pollar.mjs 181 probe the SDK endpoints the app depends on
│ ├─ verify-leg.mjs 162 read a payment off Horizon, reconcile both balances
│ ├─ fund-gas.mjs 79 send XLM so a wallet can pay network fees
│ ├─ make-testnet-account.mjs 76 funded keypair via Friendbot
│ └─ make-usdc-member.mjs 73 funded member with the USDC trustline open
│
└─ docs (repo root)
├─ README.md 287 this file
├─ SUBMISSION.md 177 the four judge questions, evidence, verify steps
├─ SECURITY.md 43 threat model for a keyless static app
├─ CONTRIBUTING.md 53 the two architecture rules, how to build, test, PR
├─ CODE_OF_CONDUCT.md 32 short, project-specific
└─ LICENSE MIT
The two biggest files tell the whole story: pollar.tsx (718 lines) is the entire bank — every
privileged interaction the app has with money, in one auditable place — and app.css
(1,287 lines) is the entire look. Everything else is smaller than its name.
Design decisions worth knowing:
- The interface is flat by construction. Hierarchy comes from type, weight and space — an Epilogue Black headline against DM Sans body — not from bordered cards. White is spent only where content genuinely needs containment: a value panel, a row that is a record, the sheet.
- Rounds are never pooled. The recipient of a round receives N−1 direct payments. This is what removes the custodian, and it is also why nothing here needs a licence to hold funds.
- We do not keep a payment ledger.
useRoundStatusasks Horizon what actually moved, and matches on sender, recipient, asset, amount and the round's time window — so a payment from last round cannot be counted twice.
Working now: non-custodial wallets behind an email login; real USDC contributions signed by each member's own wallet; round confirmation from Horizon with explorer receipts; circles shared entirely by link; and a money-in/money-out panel that reads its corridors from the SDK.
Signed out, the app is empty rather than populated with someone else's data: no placeholder stats, no account panels for an account the visitor does not have, and starting a circle asks for an account first — because a circle is built from the creator's own wallet address. The one path that deliberately needs no account is an invite link: the whole circle travels in the URL, so an invited member sees the live round and is asked to sign in only to pay.
Not solved yet, and stated plainly:
- Default risk. If a member never pays, the recipient is short that round. The chain makes the defaulter visible; it does not make them pay. Collateral or pre-funded rounds are next.
- Cash-out runs through Pollar's ramps — Bolivia (BOB), Brazil (Pix), Colombia (BreB), Mexico (SPEI), plus a SEP-24 adapter. Every one of them is Latin American: mobile money in Africa is the leg that does not exist yet, and the corridor panel says so rather than inventing a route.
- No offline/USSD entry point yet — a smartphone is required.
- Circles live in local storage plus the invite link; there is no hosted directory of circles.
- Testnet. The verified round in SUBMISSION.md moved USDC on Stellar testnet, not mainnet.
Susunaku never takes custody, never pools funds and never guarantees a payout. Anything that changed that would change the regulatory picture, so it is out of scope on purpose.
npm run dev # dev server on :5187
npm run build # typecheck + production bundle to dist/
npm run typecheck # tsc --noEmit
npm run preview # serve the built bundle
npm run member:new # mint a funded testnet member with a USDC trustline
npm run fund:gas # send XLM to a wallet so it can pay Stellar fees
npm run verify:leg # verify a payment leg on Horizon (sender recipient [minAmount])
npm run pollar:check # probe the SDK endpoints this app depends onverify:leg exits non-zero unless a matching payment exists and the recipient's balance
reconciles, so it is usable in CI as well as by hand.
Pull requests are welcome — see CONTRIBUTING.md for the setup and the rules of the road. By participating you agree to the Code of Conduct.
Found something? Do not open a public issue — see SECURITY.md. The short
version: this app never holds keys, so the highest-severity class of bug lives in the SDK and
the ledger rather than here; report anything that looks like custody, key handling, or a way to
fake a paid state as high priority.
MIT — free to fork, run and remix. The mascot is the project's identity; if you fork the code, drawing your own character is the polite thing to do.