Rotating savings circles are how a large part of the world actually saves — but they run on one person holding everyone’s money and everyone else trusting them not to disappear with it, or to skip a friend when it is their turn.
Savora removes that person. Contributions go into a vault owned by the program; Savora holds no key that can move, pause, or sweep it. The collection order is shuffled on-chain when the last seat fills — nobody picks it, not even the organiser. Once a round is funded, or its deadline passes, anyone can trigger the payout and it goes to whoever’s turn it is.


ENGINEERING CHALLENGES
A deterministic on-chain shuffle nobody controls
At seal, the collection order is permuted with Fisher–Yates. The seed is read from the SlotHashes sysvar, folded with the full 32-byte group PDA, salted with the rotation index and both bytes of the cycle counter, then expanded with SplitMix64. The salt is load-bearing: without it, a fresh rotation on a chain that has not advanced its slot hash would reproduce the previous permutation exactly. Anyone can recompute the order; no party gets to choose it.
Auto-payout in the same transaction, with a fallback
contribute detects when it is the last contribution a round needs and disburses the pot in the same instruction. The recipient token account is passed unchecked and verified against the canonical ATA derivation; if it does not exist yet, the inline payout is skipped and the permissionless crank handles it — creating the ATA there. A round can always be closed out, by anyone.
The payout cannot be redirected
The recipient is pinned at the Anchor constraint level — `address = group.members[cycle.recipient_index]` — and the destination token account is checked as that owner’s canonical ATA for the group’s mint. Every crank re-checks it. There is no admin instruction that can point a payout somewhere else because there is no admin instruction at all.
A permissionless crank that slashes no-shows
Past deadline + grace, anyone can call disburse_payout. It ejects each member who did not contribute, forfeits their deposit into the round they missed so that round’s recipient is still made whole, tombstones their seat, and records it in Group.defaulted. Membership is tracked as u16 bitmasks — seat_mask, live_mask = seat & !ejected — so none of this allocates.
The Privy confirmation race
Privy’s signAndSendTransaction runs a hard-coded 10s websocket confirmation against a different RPC node than the one that broadcast. When it loses that race it throws a bare "confirmation timed out" with no signature attached — surfacing as a red error modal on a transaction that actually landed. The fix: pass optimisticBroadcast so it returns the signature the moment broadcast succeeds, then confirm in-house with 30s of getSignatureStatuses polling over the app’s own RPC.
Routing around free-tier RPC limits
Alchemy’s free tier rejects getProgramAccounts outright and will not serve accountSubscribe / signatureSubscribe. Savora regex-detects an Alchemy URL and falls the scan endpoint back to the public devnet RPC, with a separate RPC_SCAN_URL override. A getProgramAccounts scan also skips any account whose byte length does not match the current Codama decoder — leftovers from an older program layout — rather than letting one bad decode take down the whole list.
STACK
OUTCOME
- ▸Honesty is a shipped feature. The landing page publishes its own threat model: a block producer who controls the exact sealing moment could nudge the shuffle seed. Stated plainly rather than hidden — an acceptable trade for a circle of people who know each other.
- ▸The activity feed is round-grained, not minute-grained — the program does not timestamp individual contributions, so Savora does not invent a "2h ago".
- ▸Currently on devnet. The upgrade authority is not yet burned and a v2 redeploy is pending — circles created under the old program layout are not readable by the new client.
Open to audits, contract work, and security writing.
Competitive & private audits · Secure Web3 contract work · Security writing. If you have a protocol going to production, or one that already is — let's talk.