Video Demo: https://youtu.be/JZMt14qAP-w
chameo.cash is a privacy-first, compliance-gated payout platform on Solana. Teams can pay anyone by email or social handle (X, Telegram, Discord) using direct payouts or escrowed bounties/grants. Claimants verify eligibility and claim with their own wallet without revealing wallets to each other, while on-chain links stay broken.
Payouts route through Privacy Cash to break funding and claim linkability. Dispute votes and analytics are encrypted with Inco Lightning. Aztec Noir proofs (via a relayer) prove eligibility from hashed identities and enforce one-vote nullifiers during disputes.
- Privacy Cash: unlinkable deposits/withdrawals for funding + payouts. Used in
server/src/lib/privacy-cash/andserver/src/modules/campaign/wallet.service.ts. - Inco Lightning: encrypted on-chain analytics + dispute vote totals. On-chain CPI in
contracts/programs/chameo-privacy/src/{voting,analytics}.rs; server client inserver/src/lib/inco/. - Aztec Noir + Sunspot: ZK eligibility (Merkle membership + nullifier + commitment). Circuit in
zk/noir/vote_eligibility/; proof generation inserver/src/lib/zk/vote-prover.ts; verifier invoked fromcontracts/programs/chameo-privacy/src/voting.rs. - Range: compliance screening for claim wallets. Logic in
server/src/modules/compliance/compliance.service.ts, enforced inserver/src/modules/claim/claim.service.ts. - Helius RPC: Solana RPC for mainnet/devnet in
server/src/config/solana.tsviaSOLANA_RPC_URLandSOLANA_DEVNET_RPC_URL.
| What | Visible On-Chain? | Notes |
|---|---|---|
| Host wallet | No | Funds routed through Privacy Cash |
| Campaign wallet | Yes | Receives from Privacy Cash, then deposits into the private pool |
| Claimant wallet | Yes | Receives withdrawals from Privacy Cash |
| Link: Host -> Campaign | No | Privacy Cash breaks linkability |
| Link: Campaign -> Claimant | No | Privacy Cash breaks linkability |
| Eligible identities | No | Stored as salted SHA-256 hashes |
| Votes | No | Encrypted via Inco Lightning |
| Voter wallet in ZK vote | No | Relayer pays and signs |
server/runs the API, Privacy Cash integration, ZK proof generation, and relayered voting.contracts/is the Anchor program for encrypted voting + analytics.zk/holds the Noir circuit and verifier artifacts.client/contains the UI for campaign creation, voting, and claims.
chameo/
├── client/ # Frontend helpers
├── contracts/ # Anchor program (Inco)
│ └── programs/
│ └── chameo-privacy/
│ ├── src/
│ │ ├── voting.rs
│ │ └── analytics.rs
├── server/ # API + Privacy Cash + ZK proof gen
│ └── src/
│ ├── config/
│ ├── lib/
│ │ ├── inco/
│ │ ├── privacy-cash/
│ │ └── zk/
│ ├── modules/
│ │ ├── analytics/
│ │ ├── auth/
│ │ ├── campaign/
│ │ ├── claim/
│ │ ├── compliance/
│ │ └── voting/
│ └── shared/
└── zk/
└── noir/
└── vote_eligibility/
- Host deposits into Privacy Cash.
- Campaign wallet withdraws from Privacy Cash to receive funds.
- Campaign wallet deposits into Privacy Cash to create the private payout pool.
- Claims are screened by Range and withdraw from Privacy Cash to claimant wallets.
- Server builds a Poseidon Merkle root of eligible identities.
- Voter proves membership + nullifier + ciphertext commitment via Noir.
cast_vote_zkverifies the proof on-chain and updates encrypted vote totals.- Server decrypts totals with Inco attested decrypt for final outcome.
Note: dispute resolution requires >=50% turnout of eligible identities unless forced by the server.
- Server writes encrypted analytics counters on-chain.
- Creator is granted decrypt access to read totals.
Note: On-chain analytics track view, link-click, claim-attempt, claim-success, claim-failure, and vote via server/src/modules/analytics/analytics.service.ts.
- Privacy Cash:
server/src/lib/privacy-cash/,server/src/modules/campaign/wallet.service.ts - Inco program:
contracts/programs/chameo-privacy/src/voting.rs,contracts/programs/chameo-privacy/src/analytics.rs - Inco server client:
server/src/lib/inco/client.ts - Range compliance:
server/src/modules/compliance/compliance.service.ts,server/src/modules/claim/claim.service.ts - ZK circuit:
zk/noir/vote_eligibility/src/main.nr - Merkle builder:
server/src/lib/zk/merkle.ts - ZK vote test:
contracts/tests/chameo.test.ts
Privacy Cash wallet usage (server/src/modules/campaign/wallet.service.ts):
export async function depositToCampaign(campaignId: string, amount: number): Promise<{ signature: string }> {
const keys = await getCampaignWalletKeys(campaignId);
return deposit(keys, amount);
}
export async function withdrawFromCampaign(
campaignId: string,
amount: number,
recipient: string
): Promise<{ signature: string; amount: number }> {
const keys = await getCampaignWalletKeys(campaignId);
return withdraw(keys, amount, recipient);
}Range compliance gate (server/src/modules/claim/claim.service.ts):
const compliance = await checkWalletCompliance(walletAddress);
if (!compliance.isCompliant) {
await claimsCollection().deleteOne({ campaignId, identityHash });
throw new BadRequestError(compliance.blockedReason || "Wallet failed compliance check");
}Inco ZK vote verification (contracts/programs/chameo-privacy/src/voting.rs):
require!(proof.len() == ZK_PROOF_LEN, ErrorCode::InvalidProofLength);
require!(public_witness.len() == ZK_PUBLIC_WITNESS_LEN, ErrorCode::InvalidPublicWitnessLength);
let commitment_bytes = poseidon_hash_bytes(&encrypted_vote)?;
require!(commitment_bytes.as_ref() == witness_commitment, ErrorCode::CommitmentMismatch);
let verify_ix = Instruction {
program_id: ctx.accounts.zk_verifier_program.key(),
accounts: vec![],
data: verifier_data,
};
invoke(&verify_ix, &[])?;Noir eligibility circuit (zk/noir/vote_eligibility/src/main.nr):
let leaf_fields = pack_bytes_16::<32, LEAF_FIELDS>(leaf);
let mut current = poseidon::bn254::hash_2(leaf_fields);
// Merkle path
current = hash_pair(left, right);
let secret_fields = pack_bytes_16::<SECRET_LEN, SECRET_FIELDS>(secret);
let nullifier_field = poseidon::bn254::hash_2(secret_fields);
assert(nullifier_field == nullifier);Poseidon Merkle root builder (server/src/lib/zk/merkle.ts):
async function hashPair(left: Buffer, right: Buffer): Promise<Buffer> {
return poseidonHashFields([new BN(left), new BN(right)]);
}
export async function buildMerkleRoot(leafHexes: string[], depth: number): Promise<Buffer> {
const leaves = await Promise.all(leafHexes.map((leaf) => hashIdentityLeaf(normalizeIdentity(leaf, "leaf"))));
const layers = await buildLayers(leaves, depth);
return layers[layers.length - 1][0];
}ZK vote anonymity check (contracts/tests/chameo.test.ts):
const rawKeys = message.getAccountKeys
? message.getAccountKeys().staticAccountKeys
: message.accountKeys;
const accountKeys = rawKeys.map((key: PublicKey | string) => (typeof key === "string" ? key : key.toBase58()));
assert.ok(!accountKeys.includes(voterA.publicKey.toBase58()));See server/.env.example for full list.
WALLET_ENCRYPTION_KEY: 32-byte hex key used to encrypt campaign wallet keys (required even in dev).RANGE_API_KEY: compliance screening is enforced on every claim.INCO_SERVER_PRIVATE_KEY: base64 secret key for the relayer (Inco voting + analytics).
- Chameo Privacy (Anchor) program ID (
INCO_PROGRAM_ID):- Devnet:
FsoGyYnvQDu5zXHmWHiyCxi7nWMr7RYxB1zGgz8ciJVM
- Devnet:
- ZK verifier program (
ZK_VERIFIER_PROGRAM_ID):5uFcw2nQiT2Tf7Q1zx8swugXE9rWvBQ7a3btea7qUy2d- Update this after
sunspot deployusing the pubkey fromzk/noir/vote_eligibility/target/vote_eligibility-keypair.json.
- Update this after
- Inco Lightning program ID (fixed in code + on-chain):
5sjEbPiqgZrYwR31ahR6Uk9wf5awoX61YGg7jExQSwaj - Privacy Cash program ID (
PRIVACY_CASH_PROGRAM_ID):9fhQBbumKEFuXtMBDw8AaQyAjCorLGJQiS3skWZdQyQD
ZK_MERKLE_DEPTH,ZK_CIPHERTEXT_LENGTH,ZK_PROOF_LENGTH,ZK_PUBLIC_WITNESS_LENGTHmust match the Noir constants inzk/noir/vote_eligibility/src/main.nr.
cd server
npm install
cp .env.example .env
npm run devcd client
npm install
# If the API is not on the same origin, set NEXT_PUBLIC_API_BASE_URL in client/.env.local.
npm run devBuilds the production server image and compiles Noir artifacts. The Next.js client runs separately.
docker build -t chameo-server .
docker run --rm -p 8080:8080 --env-file server/.env chameo-servercd contracts
npm testThe ZK vote test generates a Noir witness, creates a Sunspot proof, and calls cast_vote_zk against the deployed verifier program.
MIT