
Anonymous polls · verified on Stellar
Say what you really think.
Only members can answer. Each member answers one time. Nobody can see who gave which answer. This includes the admin. Anyone can count the answers again.
Prove you belong. Never say who you are.
- to prove, in your browser
- ≈ 1 s
- names or emails on-chain
- 0
- answer per member per poll
- 1
- members before a poll opens
- 5+
The problem
Other group polls fail in one way or another.
| What you need | Anonymous form | Login form | Lihim |
|---|---|---|---|
| Only members can answer | No | Yes | Yes |
| One answer per person | No | Yes | Yes |
| Nobody sees who gave which answer | Yes | No | Yes |
| Anyone can count the answers again | No | No | Yes |
| Outside people cannot add answers | No | Yes | Yes |
How it works
Five steps. You see only one touch.
- 01
Join with a passkey
Open the invite. Use Face ID or your fingerprint. You do not need a password, a wallet or a seed phrase.
- 02
Get a secret for each group
Your phone derives a new identity for this group. Only its public commitment goes into the tree.
- 03
Prove, do not tell
When you answer, your browser builds a zero-knowledge proof that you are a member. This takes about one second.
- 04
A relay sends it
A relay sends the answer for you. The relay uses no cookies and writes no IP addresses to its logs. No Stellar account is connected to you.
- 05
Anyone can count again
Stellar checks the proof and keeps the nullifier. The public verify page counts the answers again from chain data.
The zero-knowledge part
Semaphore V4, running on Soroban.
Lihim uses Semaphore V4, the anonymous-signalling protocol from PSE. Each answer comes with a Groth16 proof over the BN254 curve. The proof says: “I know the secret of one commitment in the Merkle tree of this group. This is my one-time nullifier for this poll.” The proof does not say which commitment. Try it below.
Select an answer. Then prove that you are a member.
Public signals
These go with the proof. Anyone can read them on Stellar.
- merkleTreeRoot
- hidden
- nullifier
- hidden
- hash(message)
- hidden
- hash(scope)
- hidden
Private inputs
These stay on your phone. Lihim never sends them and never keeps them.
- secretScalar
- 0xcf6a7a…c8cf
- leaf index
- 5
- siblings
- 3 hashes (dashed in the tree)
Illustrative values: the hashes on this page are stand-ins, not real Poseidon output.
The algorithm, step by step
Lihim uses the same circuits, verification keys and JS libraries as upstream Semaphore V4. Lihim adds the Stellar part: Poseidon and the LeanIMT in a Soroban contract, a Groth16 verifier on the native BN254 host functions of Stellar, and storage for nullifiers.
- 01
Identity, one per group
prf = WebAuthn-PRF(passkey, sha256("lihim/v1")) sk = HKDF-SHA256(prf, "lihim/sem/" ‖ group) (pk, secretScalar) = EdDSA-Poseidon(sk) // Baby JubjubYour phone derives it from your passkey. Thus, a new phone gets the same identities. Each group gives a different key. Nobody can connect your groups on-chain.
- 02
Commitment
C = Poseidon(pk.x, pk.y)
The public value that goes on-chain. It shows that a person joined. It does not show who.
- 03
Group tree
root = LeanIMT_Poseidon(C₀, C₁, …, Cₙ₋₁) depth ∈ 1..32, insert ≈ depth hashes
A Lean Incremental Merkle Tree in the Soroban contract, with Poseidon on-chain. When a poll opens, the contract freezes its root.
- 04
Nullifier
N = Poseidon(scope, secretScalar) scope = poll id, message = your answer
The same member and the same poll always give the same N. Thus, the contract rejects a second answer. A new poll gives a different N.
- 05
Groth16 proof over BN254
public = [root, N, h(message), h(scope)] private = [secretScalar, leaf index, siblings] h(x) = keccak256(x) >> 8 π = 8 field elements
Your browser builds it in a Web Worker. It uses the official Semaphore V4 circuits and the keys from the PSE trusted setup ceremony. There is one verification key for each tree depth.
- 06
On-chain check
L = IC₀ + Σ sᵢ·ICᵢ // bn254 g1_msm e(−A, B)·e(α, β)·e(L, γ)·e(C, δ) = 1 // pairing_check root ∈ recent roots, N ∉ used → store N
LihimVerifier does the pairing check on the native BN254 host functions of Stellar. It rejects inputs that are not on the curve or not in range. Then LihimSemaphore keeps N and emits ProofValidated.
What the proof guarantees
- The answer came from a member in the member tree of the group.
- Each member answers each poll one time (one nullifier for each poll).
- Nobody can connect your answers in different polls and groups.
- Each counted answer passed the same on-chain check. Thus, anyone can count the answers again.
What the proof does not do
- Decide who is a member. The group admin decides this.
- Hide that you joined. The admin sees the member list. The admin never sees the answers.
- Make a group of 3 people anonymous. For this reason, each group has a minimum size before a poll can open.
- Hide the live count from the chain. Lihim hides live results only in the app.
Built with
The technology.
Zero-knowledge
Semaphore V4. Each byte agrees with the official libraries.
- Semaphore V4
- Groth16
- BN254
- Poseidon
- LeanIMT
- Baby Jubjub
- EdDSA-Poseidon
- keccak256
- Circom
Stellar contracts
Three Soroban contracts in Rust: the verifier, the Semaphore groups and the polls.
- Stellar
- Soroban
- Rust
- soroban-sdk 28
- soroban-poseidon
- BN254 host functions
Keys and identity
One passkey signs you in and derives all group identities. You do not write anything down.
- WebAuthn passkeys
- PRF extension
- HKDF-SHA256
- Ed25519 admin keys
- @noble/curves
Client and SDK
A PWA with the prover in a Web Worker. Other Stellar apps can use the SDK.
- Next.js 16
- React 19
- TypeScript
- Tailwind CSS 4
- Radix UI
- @lihim/sdk
- @semaphore-protocol/proof
- @stellar/stellar-sdk
Answer relay
It pays the fees and sends the proofs. It uses no cookies and no auth header, and it does not log IP addresses.
- Hono
- Node.js
- simulate-then-submit
Off-chain data
Names and invites stay here, behind row-level security. Answers never stay here.
- Supabase
- Postgres + RLS
- Edge Functions (Deno)
- pg_cron indexer
Rules
Lihim will never do these things.
Move money
No payments, no prizes, no bets and no paid answers. Never.
Put your name on-chain
On-chain, there are only roots, commitments, nullifiers, poll ids and answers. Names and emails stay off-chain. Only your group admin can see them.
Promise “100% anonymous”
We tell you exactly who can see what. In a small group, people can guess an answer. For this reason, each group has a minimum size.

Your group knows the answer. Let the members say it.
Create a group. Share one invite link. Ask the question that nobody asks aloud.