TL;DR: I built an eight-player social deduction game where confidential token transfers are the clues. Along the way: the mental model that makes Stellar’s confidential tokens click, and four gotchas waiting for the next builder.
Confidential tokens have been on Stellar’s horizon for a while, and as of June 2026, developers can start playing around with them on testnet. So, naturally, I used them to build a game about murder, suspicious villagers, and one hungry werebear.
Before we begin! Please note that confidential tokens are currently a developer preview, only available on Stellar’s testnet (as of this writing in September 2026). The current implementation is intended for experimentation. Who Ate Gerald? is a personal project and has not been audited or reviewed as a reference implementation. The confidential token contracts and Noir circuits were built by OpenZeppelin, and the UltraHonk verifier was built by Nethermind. Keep up to date on all things confidential tokens in the Stellar developer docs.
Who Ate Gerald? is an eight-player social deduction game in which confidential token transfers serve as clues. Players make private purchases, investigate each other’s spending, accuse their neighbors, and try to identify the werebear before the village population gets wiped out.
In this piece, I’ll cover how the game works, the mental model that made confidential tokens click for me, and the hiccups I hit along the way, hopefully saving the next developer some trouble. Games may not be what this feature was built for, but the mechanics apply directly to more serious applications.
What is Who Ate Gerald?
Let’s start by talking briefly about the game, without getting too in the weeds on mechanics (as a gamer, I have way too much fun with that part). To begin, players log in to Freighter, choose a character, and are secretly assigned as either a villager or a werebear.
During the day, each player shops for items at four stores. Items can protect you, expose other players, or just create chaos. You can even buy Gerald’s fingers (Gerald, unfortunately, was the first villager eaten by the werebear. That’s how everyone knows there’s a werebear in their midst).
Every purchase is a real testnet XLM transfer through a confidential token contract. The blockchain shows that a player paid a particular shop, but not how much they spent or which item they bought.
Once everyone is finished shopping for the day, each player can ask Maude McLedger, the contract’s auditor, one question about other players’ purchases. Then the accusations begin: players can chat, argue, and vote on who they think the werebear is. Every night, one player is banished by the village, and one player is eaten by the werebear. And the next day, they all do it again.
Under the hood, the game uses every major step of the confidential token lifecycle: registration, deposits, merges, transfers, withdrawals, and auditor access. More on all of these below.
There’s one important piece of context before I get into how all of that works: I’m a vibe coder. I wasn’t a developer before Claude and other AI models entered our lives, and I relied heavily on AI to build this game. I gave it the confidential token sources it needed, explained the game mechanics, and let it do its thing.
The problem is that Claude could confidently tell me it had built a working confidential token transfer flow, but I had no way to know whether that was actually true until I understood the flow myself. So I started digging into what the game was doing under the hood, and that process is basically how this article came to exist.
So, here’s how it all works:
The three places value lives
1. Your regular balance
This is real XLM held by your Stellar account. In Who Ate Gerald?, this would be the amount of XLM you have in your Freighter wallet. The amount is public.
2. The contract pool
The confidential token contract acts as a wrapper around regular XLM: it holds all deposited XLM in one shared pool. Every deposit adds real XLM to this balance. That XLM stays in the pool until someone withdraws it.
The pool’s total balance is public, but its internal ownership breakdown is not. An observer can see how much XLM the wrapper holds without seeing how much belongs to each participant. Also, note that you can use any SEP-41 asset, but I used XLM in the game, so I’ll refer to it throughout this article.
3. Your confidential claims
The contract maintains an internal ledger entry for every registered participant. Your entry contains:
Your public keys
Your auditor configuration
A pending balance
A spendable balance
The balance fields do not contain readable numbers. Each is stored as a Pedersen commitment: a balance combined with a large random secret, represented as a point on an elliptic curve.
Anyone can read the commitment bytes onchain, but those bytes do not reveal the balance. Only someone with the opening (the original amount and its random secret) can interpret the commitment.
Your device must therefore track the information needed to open and update your commitments.
Pending and spendable balances
Each account has two confidential balance fields, all represented by claims you have on the contents of the pool.
Pending (also known as Receivable): your inbox
Deposits and incoming transfers are added to your pending balance.
Other participants can increase this balance by paying you, but they cannot spend from it or affect your spendable balance.
You cannot spend directly from pending. You must first merge the entire pending balance into the spendable balance.
Spendable: your pocket
Transfers and withdrawals come from your spendable balance.
Only actions authorized by you can change it.
Why the balances are separated
A zero-knowledge proof is only valid against the exact commitment that existed when the proof was created. That creates a problem if incoming transfers go straight into your spendable balance: you could start constructing a payment proof, receive a tiny payment from someone else before submitting it, and suddenly your balance commitment has changed, making your proof invalid.
Worse, someone could intentionally exploit that by repeatedly sending you tiny payments and preventing your transactions from ever completing. The pending balance prevents this by absorbing changes caused by the outside world. Your spendable balance stays stable and only changes when you act.
The complete confidential token transfer flow
A first confidential payment touches all six steps of the lifecycle:
Register: Each participant registers with the wrapper contract. (1 tx each)
Deposit: The sender deposits public XLM into the contract pool.
Merge: The sender moves their pending claim into their spendable balance.
Transfer: The sender confidentially transfers part of their spendable claim to the receiver’s pending balance.
Merge: The receiver moves the incoming claim from pending to spendable.
Withdraw: The receiver may exchange part of their confidential claim for regular XLM.
Step 1: Register
Each participant registers once per wrapper contract.
Registration binds the participant’s public keys and auditor information to their account in the contract’s internal ledger. It also creates their pending and spendable commitments, both initialized to zero.
The participant submits a standard Stellar transaction containing the required keys and a small zero-knowledge proof demonstrating that the registration information is valid.
On an explorer, registration looks like a standard contract invocation.
Step 2: Deposit
The sender deposits regular XLM into the wrapper contract.
The amount is public. Observers can see:
Who deposited
When the deposit happened
How much XLM was deposited
The deposited XLM leaves the sender’s regular account and enters the contract pool. In exchange, the contract adds an equal claim to the recipient’s pending commitment.
It is useful to think of this as an exchange rather than a simple transfer.
The XLM remains real XLM. What becomes confidential is the ownership claim representing who controls that portion of the pool.
Step 3: Merge pending into spendable
The sender must move their pending balance into their spendable balance before they can make a confidential payment.
Only the account owner can authorize this operation.
The merge is all or nothing. It combines the entire pending commitment with the spendable commitment through homomorphic addition: the contract can combine two hidden values without learning either value or their sum.
After the merge:
Pending returns to zero.
Spendable contains the combined balance.
No amount is revealed.
Step 4: Make a confidential transfer
The sender’s device performs the private arithmetic and constructs a zero-knowledge proof.
The proof demonstrates that:
The sender had enough spendable value.
The sender’s balance was reduced correctly.
The receiver’s balance was increased by the same amount.
No value was created or destroyed.
The proof does not reveal the transfer amount or either participant’s balance.
When the transaction confirms:
The sender’s spendable commitment decreases.
The receiver’s pending commitment increases.
The underlying XLM remains in the shared contract pool.
Once for each auditor involved: the recipient’s auditor gets the amount, and the sender’s auditor gets the amount plus the sender’s new balance.
To everyone else, the encrypted values and commitments appear as meaningless bytes.
The transaction record still reveals that the sender interacted with the receiver and when the transfer happened. It hides the amount and balances, not necessarily the participants’ identities.
Step 5: The receiver merges
The transferred claim belongs to the receiver as soon as the confidential transfer confirms, but it initially sits in the receiver’s pending balance.
The receiver must authorize a merge before spending or withdrawing it.
This is the same operation the sender performed earlier.
Receiving is passive. Using the received value requires the receiver to merge it at a time of their choosing.
Step 6: Withdraw
A withdrawal exchanges part of a confidential claim for regular XLM.
The withdrawal amount becomes public again.
The withdrawing participant supplies a proof showing that their spendable commitment contains enough value. When the transaction succeeds:
Their spendable claim decreases.
Real XLM leaves the contract pool.
The XLM enters the destination account’s regular balance.
Inside the wrapper, value can circulate confidentially. To use that XLM elsewhere on Stellar, the holder must withdraw it through this public boundary.
The auditor
I’ve described the sender, the receiver, and the public. There’s one more party: the auditor.
An auditor is an encryption keypair, not a Stellar account. Its public key is published in an onchain auditor registry, and when an account registers with the token contract, it picks which entry in that registry audits it. That choice is permanent. From then on, every transfer the account sends or receives must include its amount encrypted to that auditor’s key (those are the auditor copies from Step 4), and a transfer without valid auditor copies is rejected by the proof. Whoever holds the matching secret key can open every amount for the accounts bound to it and pair each one with the sender and receiver that were public all along.
Who Ate Gerald?’s registry has exactly one entry, so Maude McLedger, the village fortune-teller, audits every villager and every shop.
Gotchas
The curious case of the empty explorer page
In my game, the Blacksmith (stellar.expert) has received 320 confidential payments. Before I automated the merges, the page showed only three transactions despite hundreds of incoming payments. My first thought was that the system had simply broken.
Here is the thing about confidential transfers: the sender handles everything. They submit and authorize the transaction, while the recipient doesn’t have to sign anything. The wrapper contract updates the recipient’s receiving balance during that sender-side call.
Even though the contract call identifies the receiver, an account page lists only transactions where that account is the source or a signer; a recipient named only as a contract-call argument never appears. This means a recipient’s page might look completely dormant even if they are raking in tokens. Activity only surfaces when the recipient steps in to merge or withdraw funds.
ZKP requirements will dictate your architecture
Confidential token operations generally fall into two distinct camps:
Operations that require zero-knowledge proofs (Registration, Transfer, and Withdrawal).
Operations that are just authorized calls (Deposit and Merge).
The SDK makes this split very clear: deposit and merge only need a signer, while register, confidential_transfer, and withdraw demand a heavy proof.
This technical divide determined exactly where my game logic lives. Generating proofs is resource-heavy; it works fine in a browser or a Node.js script, but it is a non-starter for 128 MB serverless workers. I had to design my entire flow around these memory constraints.
In Who Ate Gerald?, players generate proofs for their own transfers client-side. Every morning, the game server signs merges for the shops (no proof needed!). Since withdrawals require proofs, they stay outside the main game loop in a script I trigger manually.
Derivation, identity, and the key recovery hurdle
Registration locks a confidential key hierarchy to a specific account. Your application has to be able to recreate those exact keys every time the user returns.
Who Ate Gerald? derives these keys from a deterministic wallet signature over a fixed message. Because the signature is repeatable, the same wallet always produces the same key material. But things got messy when I looked into passkeys.
First, WebAuthn assertions aren’t naturally stable for key derivation because they use random challenges and nonces. While the WebAuthn PRF extension is the long-term solution, support is currently spotty. Second, on Stellar, a passkey wallet is usually a new contract account. Since registration is bound to a specific address, moving a user to a passkey account makes them a “new” participant. Their old balance won’t follow them without a public withdrawal and deposit.
Service accounts don’t merge themselves
Confidential payments land in the pending balance and stay there until a merge is authorized. If no one takes that action, the tokens just pile up indefinitely.
Since my shops are automated service accounts, they sat for a month accumulating tokens because I hadn’t written the code to make them merge. By the time I fixed it, they had gathered 19,500 XLM that were totally unspendable. Nothing was lost, but the shops’ public explorer pages remained empty, hiding the wealth they had technically “received.”
Try it, inspect it, or reference it for your own demo!
Disclaimer
Frontier Lab is published by the Stellar Development Foundation. Posts are for educational purposes only and are not financial, legal, or investment advice. The SDF Privacy Policy applies to this publication’s subscriber list and comments. Substack’s Privacy Policy covers Substack’s own use of your Substack account data. Comments are subject to the Stellar Code of Conduct, and we may remove comments that are off-topic, promotional, or misleading.




