An on-chain ROSCA for private communities. The social side stays; the money stops depending on one person.
| Live app | https://arisan-cook.vercel.app |
| Program | Dwd7DXUQHRJaj1suYz6fTcVW7JJqBFVztg1z77t6Ysg |
| Network | Cookie Chain mainnet |
| Wallet | Nightly |
Open source under the MIT License.
Get paid, set some aside, do not touch it. Simple, and it is exactly where most people fail — not because they cannot save, but because staying disciplined is hard when nothing holds you to it.
So communities built something that does. A ROSCA — rotating savings and credit association, arisan in Indonesia — is a group who agree an amount and a period. Everyone pays the same in each round, and one member takes the whole pot. Next round, someone else. When everyone has had a turn it is over, and each person has put in and taken out the same.
What that buys is the thing a savings account cannot give you: social commitment. You keep paying because the group is watching, and because your own turn is still coming.
It is also two products at once, depending on where you land in the order:
| Your turn | What it is |
|---|---|
| Early | Interest-free credit — a lump sum now, repaid over the remaining rounds |
| Late | Committed saving — money you would not have set aside on your own |
Tens of millions of Indonesian households run on this. It works because the group already knows each other.
Not the idea. The coordination.
Somebody has to collect from everybody, chase whoever is late, keep the record of who paid and who missed, decide whose turn it is, and hold the pot in between. That person is doing unpaid admin every single round — and while they hold the money, the whole group is relying on one person's honesty and one person's spreadsheet.
That is a chore and a single point of failure at the same time. Neither is a problem with arisan. Both are problems with doing arisan by hand.
This puts that job on chain and leaves everything else alone.
The organiser no longer holds the pot — a program account does, with no key. The rules are written once when the arisan is created and cannot be changed after anyone has committed money. Contributions, deposits, whose turn it is, the payout and the full payment history all happen in the program, in the open.
The deposit is the part that covers a missed round, so one person paying late does not quietly become everyone else's problem to sort out.
The social trust between members stays exactly where it was. You still join a circle because you know these people. What changes is that the money no longer depends on any one of them.
The deposit is a buffer for paying late, not insurance against leaving. This is the part we got wrong twice, so it is worth being exact.
Miss a round and the program takes that round's amount out of your own deposit and puts it in the pot. The round still pays in full, nobody else covers you, and your books record a miss rather than a payment. Run the deposit down and you are sidelined from collecting until you top it back up. Two contributions is the sensible size: you can be late twice.
What it does not do is stop the member who takes the first pot and disappears. That cannot be fixed with a deposit, and the arithmetic says why:
| seats | still owed after turn 1 | deposit | covered? |
|---|---|---|---|
| 3 | 2 × contribution | 2 × contribution | yes |
| 10 | 9 × contribution | 2 × contribution | no |
| 20 | 19 × contribution | 2 × contribution | no |
Closing that gap means a deposit larger than the pot — which means only members
who never needed the circle could join. The first version of this demanded
exactly that, contribution × seats from everyone up front, and it was useless:
you had to already own the pot in order to be allowed to receive it.
So the group carries that risk, the way every arisan always has. This is built for a private circle of people who already know each other and have already agreed to save together, which is why rooms are invite-only. It is not a trustless lending protocol and does not pretend to be one.
What the chain contributes is the part a spreadsheet and a group chat do badly:
nobody holds the money, the rules freeze the moment people commit, the draw
cannot be timed or steered, and the books are public and permanent.
scripts/test-community-deposit.mjs proves all of it against mainnet, including
the gap above.
The draw cannot be timed. Requesting a round's draw commits it to the hash of a block three slots in the future. At the moment it is called, nobody, including the organiser, can know who wins. That matters more here than in a game: the organiser is a participant.
An absent organiser cannot stall it. Running the draw and charging a missed round to somebody's collateral are both permissionless. In the test suite the wallet that charges the defaulter is not even a member.
The rules are frozen. Contribution, collateral, seats and round length are written once at creation and can never be changed. Nobody has to take the organiser's word that the terms will hold, because the organiser cannot move them either.
The books are public. Who paid, who missed and how many times, who has already had a turn, how much collateral each person still holds. In a real arisan that is a notebook one person keeps.
And the gas is covered where sponsorship is available. Joining, paying a round and taking your turn can be fee-free; creating a campaign still needs the creator's wallet for account rent. The contribution and any collateral always come from the member's own wallet, because a stake somebody else paid for guarantees nothing.
A full cycle on mainnet, including a member going quiet halfway through. Three members, 10 COOK a round. The deposits in this run were 30 COOK — sized under the earlier rule, before it came down to two contributions — but what it demonstrates is unchanged: a missed round comes out of the absentee's own deposit.
| Round 1 | Round 2 | |
|---|---|---|
| Members who paid | 3 | 2 |
| Pot paid out | 30 COOK | 30 COOK |
The pot was full both times. In round 2 the shortfall came out of the absent member's reserve:
| Before | After | |
|---|---|---|
| Defaulter's reserve | 30 COOK | 20 COOK |
| Pot | 20 COOK | 30 COOK |
The missed round was settled from their reserve and recorded as a miss, while
the reserve still covered their future obligations. The draw that settled it:
4aX6u5FB…
Also verified on mainnet, all as refusals: a fourth member cannot squeeze into three seats, nobody can join once it is running, paying twice in one round is rejected, drawing before the round closes is rejected, the same absence cannot be charged twice, and a member cannot collect two turns.
Open Get demo COOK, claim, then Join with code using ARISAN-DEMO,
which is published on purpose. Rounds there last one minute, so you can pay,
watch the draw and collect in a single sitting.
Three things about that room are the result of walking the path a stranger actually takes, rather than the path its author already knows:
- Two seats, not three. A non-creator may only start a circle once every seat is taken. A three-seat room holding one of ours plus one visitor left that visitor stuck: reserve posted, circle unstartable, nothing to do but leave. Two seats means your arrival fills the room and you start it yourself.
- The other seat will not pay. That is deliberate. When the round closes, the public ledger offers you Cover from reserve — you settle the absent member out of the reserve they posted, the pot stays whole, and their books record a miss rather than a payment. It is the safety rule, run by you, on your first visit.
- One code, several rooms. A room mid-cycle has nothing to offer someone who just arrived, so the app sends you to whichever one still has a seat.
If the draw lands on your seat, you collect the pot. If it lands on ours, a crank collects it within the minute and the circle moves on.
The public app exposes a read-only /api/metrics
endpoint for developers and reviewers. It reads Cookie Chain directly and reports
active TVL, protected TVL, room/member counts, contribution volume, payout volume,
and indexed transaction counts with an asOf timestamp. TVL excludes vault rent,
the faucet and relayer balances; volume is derived from confirmed contribute,
slash_absent, and claim_turn transactions.
Circles are addressed by invite code rather than listed as an open pool. A
creator opens a campaign with a short description and a public social-post URL.
The program stores those details in a CircleRoom PDA alongside the SHA-256 hash
of the generated invite code. The creator is automatically the first member, then
shares the code with the people who saw the post.
What the code actually is. It is a discovery key, not an access-control
boundary, and the demo is deliberately built so you can see that for yourself.
join_circle takes the code hash as its argument and compares it to the hash
stored in the room, and that stored hash is a public account anyone can read. So
the honest statement is: anyone who can construct a transaction can join any
room, without ever knowing the code. The gate is in the interface, which asks for
a code before it will show you a room, not in the program.
That is a deliberate choice for a demo — a reviewer should be able to inspect a room and join one without hunting for a secret. Enforcing it properly would mean a creator-signed approval or a Merkle root of invited wallets, which is the right shape for real money and the wrong shape for something people are meant to try in one sitting.
Circle accounts created before rooms existed remain readable; their creator must run
scripts/seed-demo-circle.mjs or configure a room before new members can enter.
One thing to know before you try it. Wallet-adapter's signTransaction does
not forward a chain identifier, and Nightly does not publish Cookie Chain through
the Wallet Standard even while pointed at it. So a wallet may preview against the
wrong network and warn that the transaction will fail. It will not. A signature
covers the transaction bytes and says nothing about which chain it runs on.
TVL is locked by the design, not by hoping people stay. Each member's reserve sits for the whole cycle and contributions sit for a round. A 10-seat circle at 10 COOK a round holds 1,000 COOK of reserve continuously plus up to 100 COOK of pot; unused reserve returns to its member when the circle finishes.
Volume is scheduled rather than hoped for. A 10-seat circle over 10 rounds is 100 contributions, 10 draws and 10 payouts. Roughly 120 transactions per circle, arriving on a timetable the participants already committed to.
That matters on this chain in particular. Cookie Chain carries very little traffic, so an app that waits for it gets none. An arisan generates its own, on a timetable its members already committed to — and the effect is large enough to see from outside:
node scripts/chain-share.mjsTwo 12-hour windows, a day apart, both measured with that script:
| chain-wide, non-vote | from this program | share | |
|---|---|---|---|
| 2026-09-15 | 553 | 238 | 43% |
| 2026-09-16 | 1,865 | 376 | 20% |
The share fell while our own output grew, because the rest of the chain got roughly three times busier in a day — other entries in this bounty ramping up before the deadline, most likely. Which is the point of quoting the script rather than a number: any figure here goes stale, and a stale figure a reviewer can disprove is worse than none. Run it yourself for today's.
Cookie Chain publishes no stats API (the explorer's api.cookiescan.io is a
Metaplex DAS endpoint for tokens and NFTs), so that script reads the RPC's own
getRecentPerformanceSamples and compares it against this program's signatures.
Vote transactions are excluded throughout: on a Solana fork they are consensus
overhead rather than usage, and counting them would make a quiet chain look
busy.
What sponsoring costs. The fee is 10,000 lamports, two signatures at 5,000 each. Joining also opens a 99-byte member record, 1,517,280 lamports of rent. So a new member costs 0.001527 COOK once, and every round they pay after that costs 0.00001.
Some of the circles on the live app are operator-seeded: one person funded the
wallets, and scripts/seed-circles.mjs opened the circles and filled the seats.
They are not strangers who found the app.
Calling that organic demand would be a lie, so here is what it actually is. The
COOK is real, locked in real program vaults under the same rules as anyone
else's — the reserve cannot be pulled out while a circle is running, a missed
round is slashed the same way, the draw is the same slot-hash draw, and
/api/metrics counts it with the
same code it would count a stranger with. What it demonstrates is that the
mechanism runs unattended and that money stays locked for the full cycle. What
it does not demonstrate is that anybody wants it yet.
The scripts are in the repository rather than hidden, because the point of publishing a number is that somebody can check how it was made:
node scripts/seed-circles.mjs --dry-run # the plan and what it costs
node scripts/crank-circles.mjs --status # where every circle currently stands
node scripts/unwind-circles.mjs --status # what is still locked, and until when organiser ──creates──▶ Circle rules frozen at creation
│
members ──join, post──▶ Bond vault collateral, theirs until they default
│
──contribute──▶ Pot vault ──▶ the drawn member
▲ one turn each, then done
│
slash_absent a missed round moves from Bond
to Pot, so the round still pays
out in full
request_turn ──▶ commits to a block 3 slots ahead
finalize_turn ─▶ reads that block's hash nobody can time it
both permissionless, so an absent organiser cannot stall the circle
Contributions and collateral are separate vaults on purpose. Collateral belongs to the member until they default; it must never be payable as somebody else's pot.
Cookie Chain's gas token is native COOK, so there are no SPL token accounts and no ATAs. Vaults are zero-data PDAs holding lamports.
The program is built for Cookie Chain's SVM runtime with Anchor and the Solana
web3 SDK. The app reads and submits transactions through Cookie Chain's native
RPC at rpc.cookiescan.io, and links every live
transaction and program account to CookieScan.
Cookiebox and Cookieswap are not required by this product: Arisan moves native
COOK into program vaults and does not swap tokens or provide liquidity.
cookie-mcp is useful for local agent/developer workflows, but it is not a
runtime dependency of the public app and no private key is sent to it.
The organiser has no privileged access to money. They can create a circle and start it. That is all. No instruction lets them move the pot, the collateral, or anybody's membership.
Collateral cannot be paid out as a prize. It lives in a separate account from the pot, and the only instruction that moves it into the pot charges a specific member for a specific missed round, capped at one contribution.
A missed round can only be charged once. Slashing marks the round settled for that member, so the same absence cannot be billed repeatedly to drain them.
A default cannot make the group absorb a loss. New circles must lock a
reserve of contribution × seats per member before starting. If that reserve is
not available, the program refuses to draw and refuses to pay out until the
shortfall is repaired. The reserve is reduced only when it actually covers a
missed contribution; unused COOK remains the member's to withdraw at the end.
Eligibility is checked when the pot is collected, not when it is drawn. The claimer must hold the drawn seat, not have had a turn, be active, and have settled this round. Drawing a seat proves nothing on its own.
A stalled turn can be redrawn. If the drawn member never collects, anyone can redraw after the claim window, so one absent person cannot freeze everybody else's money.
A stolen relayer key cannot drain anything. The relayer is a fee payer with no authority anywhere in the program, and reclaims at most 0.005 COOK per transaction, enforced on chain.
Governance keys must be multisig. The program upgrade authority and config
authority should be moved to a 2-of-3 governance wallet before material TVL;
the creator has no withdrawal instruction for the pot or bond vault. A 3-of-3
threshold is intentionally not recommended because one unavailable signer would
freeze recovery. See docs/MULTISIG-RUNBOOK.md.
Honest limitation: the draw is a slot-hash commitment, not a VRF. A block producer controlling the target slot could bias it. Acceptable at these amounts, written down rather than hidden, and replaceable in one function.
Second honest limitation: nothing verifies identity, so one person can hold several seats under different wallets. Collateral prices that rather than preventing it, which is the trade-off every on-chain ROSCA makes.
Everything runs against mainnet with real COOK.
node scripts/test-arisan.mjs # a full cycle including a defaulting member
node scripts/test-relayer.mjs # seven attacks on the relayer, then the real path
node scripts/test-flows.mjs # the savings-jar instructions the program also carries
node scripts/e2e.mjs # a zero-balance wallet taking partCurrent results: 22/22 arisan, 9/9 relayer, 17/17 jars, 6/6 gasless.
Bugs found by running against a live chain rather than a local validator, all fixed:
- Anchor billed account rent to the user, which broke the one thing the sponsorship promises. Paying and authorising are now separate accounts.
- A draw pinned to an exact slot could be stranded forever, because Solana skips slots. It now takes the first block at or after the target, inside a bounded window so a late finalizer cannot reach back for a hash they already saw.
- The rent estimate double-counted the account discriminator, quietly moving 121,360 lamports per action from the sponsor vault to the relayer. Caught by reading the balance deltas of a real user's transaction, not by a test, which had the same wrong constant in it.
The program also carries instructions for savings jars, giveaway envelopes and fundraising campaigns, all deployed and tested, all hidden from the interface. They were explored on the way here and set aside rather than deleted.
Rotating savings on chain is not a new idea and this does not claim to be first. The design follows the pattern established by Akyba Protocol on Cardano, whose ROSCA spec is the clearest public writeup of it: immutable group parameters, collateral slashing for missed rounds, a rejoin path rather than permanent exclusion, and a permissionless distribute crank. Others in the space include Bakso Finance on Solana and Njangi on Sui.
What is here that is not there: it runs on Cookie Chain, and sponsored actions can let a member with an empty wallet join, pay and collect; the COOK being contributed still has to come from that member.
git clone https://github.com/ranimth0707/arisan.git
cd arisan
npm --prefix app install
RELAYER_SECRET_KEY="$(cat ~/.config/solana/cookiejar-relayer.json)" \
FAUCET_SECRET_KEY="$(cat ~/.config/solana/cookiejar-faucet.json)" \
node relayer/dev-server.js
npm --prefix app run dev # http://localhost:5174Deployment is app/ as the Vercel root directory. app/api/*.js become the
serverless relayer and demo faucet, and app/dist is the site.
RELAYER_SECRET_KEY is required. FAUCET_AMOUNT_COOK defaults to 0.5 and is
capped at 1.
FAUCET_SECRET_KEY should point at a different wallet from the relayer.
Wallet addresses cost nothing to generate, so no per-request limit can stop
someone from draining a faucet; what a separate wallet decides is whether the
drain also takes the sponsor down with it. Keep the faucet wallet small and top
it up. If the variable is unset the faucet falls back to the relayer's wallet
and holds back a 20 COOK reserve, which keeps the gasless buttons working but is
a fallback, not the arrangement to ship.
On top of that the faucet keys its rate limits on the connection and the
recipient separately — never on the pair, which would hand every fresh address
its own quota — and refuses any wallet already holding 2 COOK, since swept funds
have to land somewhere. scripts/test-faucet.mjs asserts all of it against a
running relayer.
Two things that will bite on a fresh Vercel project. @solana/web3.js pulls in
rpc-websockets, which require()s a version of uuid that is ESM only, so the
function dies at cold start with ERR_REQUIRE_ESM; the overrides block in
app/package.json pins it back. And a newly aliased *.vercel.app subdomain sits
behind deployment protection until it is added as a project domain rather than a
bare alias, which 302s every visitor to an SSO page.
Rebuilding the program needs Anchor 1.2 and the Solana CLI, and Cookie Chain runs an older core than the current CLI, so order matters:
anchor build # regenerates the IDL, but a v3 binary
node scripts/sync-idl.mjs # IDL and types into app/
cargo-build-sbf --arch v0 --manifest-path programs/cookie_jar/Cargo.toml
solana program deploy target/deploy/cookie_jar.so \
--program-id Dwd7DXUQHRJaj1suYz6fTcVW7JJqBFVztg1z77t6Ysganchor build leaves an SBPF v3 binary that this chain rejects, so the v0 build
must come after it. Upgrade by address, not by the keypair file: the loader
does not support ExtendProgram, and a wiped target/ silently regenerates a
different program keypair and deploys a stranger to a new address.
Deploying somewhere fresh also needs these once:
node scripts/fund-deployer.mjs 2000 # move COOK to the deploy wallet
node scripts/initialize.mjs # create the Config PDA, register the relayer| Program | Dwd7DXUQHRJaj1suYz6fTcVW7JJqBFVztg1z77t6Ysg |
| Config PDA | 3zuBJYMEhLvqo996KSQbNMokPQASeVy7Ps7TeV8ruvci |
| Relayer | BMS3jCCznvmLcFpiTSPPLZtDYsmAwS68AXt85g6DhpeL |
| RPC | https://rpc.cookiescan.io |
| Explorer | https://cookiescan.io |
Anchor 1.2 on Cookie Chain (SVM), React and Vite for the app, Solana Wallet Standard for wallet discovery so Nightly registers itself, and a serverless relayer that signs as fee payer and nothing more.
- No identity, so one person can hold several seats. Collateral prices it rather than preventing it.
- No commission for whoever runs the draw, so cranking it is currently altruism or self-interest. Akyba pays a percentage for exactly this reason.
- Round length is fixed for the whole cycle. Real circles sometimes renegotiate.
- The draw is a slot-hash commitment, not a VRF.
- Native COOK only. No SPL tokens.