Building Private Pass: Token-Gated Access with Zero-Knowledge and Token Extensions on Solana
Prince Israel
TL;DR
I built Private Pass, a simple demo showcasing private token-gated access on Solana. Users claim tokens, prove eligibility through zero-knowledge, and access exclusive content, all while maintaining privacy. I shipped a polished concept demo in less than 48hrs, learned the hard limits of TypeScript for confidential transfers, and discovered what it takes to build real privacy infrastructure on Solana.
Live Demo: Try it yourself | GitHub: Source Code
The Vision: Privacy Meets Access Control
Imagine this: You hold VIP tokens for an exclusive event. You want to prove you’re eligible for early access, but you don’t want everyone to know exactly how many tokens you have. Maybe you’re a whale holding 10,000 tokens. Maybe you have exactly the 100-token minimum. Nobody should know, except that you meet the threshold.
This is the promise of confidential token-gated access: prove you belong without revealing your holdings.
On Solana, Token-2022’s Confidential Transfer extension makes this technically possible. But how hard is it to actually build? What are the trade-offs? Can you ship something in TypeScript, or do you need to go full Rust?
I set out to answer these questions.
What I Built
The User Experience
Private Pass is a clean, minimal demo with four steps:
Connect Wallet: Users connect Phantom/Solflare
Claim Tokens: Mint 100 PASS tokens to their wallet
Generate Proof : Create a zero-knowledge proof of eligibility
Access Granted : Unlock exclusive content
The entire flow takes about 30 seconds. No complicated setup, no manual airdrops, just connect and go.
The Tech Stack
Frontend: Next.js 14 + TypeScript + Tailwind CSS
Blockchain: Solana Token-2022 (Devnet)
Styling: Framer Motion + Glassmorphism
Scripts: TypeScript (tsx runner)
Why did I choose this stack?
Well Next.js provides that fast iteration that makes its DX great as well as easy deployment. Token-2022 provides a future-proof, extension system ready confidential transfers that enable tokens transfer between token accounts without revealing the transfer amount.
The Architecture
Token Infrastructure
I created a simplified token system with three main components:
1. Token Creation Script (scripts/token/create-mint.ts)
// Create Token-2022 mint on Solana devnet
const mint = Keypair.generate();
const transaction = new Transaction().add(
SystemProgram.createAccount({...}),
createInitializeMintInstruction({...})
);
What it does:
Generates a new Token-2022 mint
Sets decimals to 2 (100 base units = 1 token)
Saves config to
token-config.jsonCreates mint authority keypair
To Run it:
npm run setup:token
2. Claim API Endpoint (app/api/claim/route.ts)
export async function POST(request: NextRequest) {
// 1. Validate wallet address
// 2. Check rate limit (24hr cooldown)
// 3. Create token account if needed
// 4. Mint 100 tokens to user
// 5. Return transaction signature
}
Key features:
In-memory rate limiting (24-hour cooldown)
Auto-creates token accounts
Mints to public balance (limitation we’ll discuss)
Returns transaction link for verification
3. Balance & Proof Functions (lib/solana.ts)
// Query user's token balance
export async function getTokenBalance(
walletPublicKey: PublicKey
): Promise<number | null> {
const account = await getAccount(...);
return Number(account.amount);
}
// Simulate proof generation
export async function generateZKProof(
walletPublicKey: PublicKey,
threshold: number = 100
): Promise<Proof> {
// Verify balance first
const hasBalance = await checkEligibility(...);
// Generate mock proof structure
return { publicInputs, proof, timestamp };
}
Frontend Flow
The UI follows a progressive disclosure pattern:
// State management
const { connected } = useWallet();
const [hasProof, setHasProof] = useState(false);
// Reset on disconnect
useEffect(() => {
if (!connected) setHasProof(false);
}, [connected]);
Step visibility logic:
Step 1: Show only when
!connectedStep 2: Show only when
connectedStep 3: Show only when
connected && !hasProofStep 4: Show only when
connected && hasProof
This creates a clean, focused experience where users only see relevant actions.
The Big Limitation: TypeScript vs. Rust
It was pretty great but reality kinda struck at some point.
What I Really Wanted to Build
True confidential transfers following Solana’s official Rust example:
// From Solana's confidential transfer example
let elgamal_keypair = ElGamalKeypair::new_from_signer(...);
let aes_key = AeKey::new_from_signer(...);
let proof_data = PubkeyValidityProofData::new(&elgamal_keypair)?;
configure_account(
token_account,
&decryptable_balance.into(),
proof_location,
)?;
This involves:
ElGamal encryption for hiding amounts
AES encryption for decrypting your own balance
Zero-knowledge proofs for validating operations
Proof context state accounts for on-chain verification
What I Actually Built
Standard Token-2022 mint with simulated privacy:
// Yes We can create the mint
const mint = Keypair.generate();
await createInitializeMintInstruction(...);
// But we CAN'T do the following in TypeScript:
// ElGamalKeypair generation
// AES key derivation
// ZK proof creation
// proof verification
// So we mint to PUBLIC balance instead
await createMintToInstruction(...);
Why TypeScript Doesn’t Work
The libraries required for confidential transfers only exist in Rust:
# Available in Rust
solana-zk-sdk = "1.18"
spl-token-confidential-transfer-proof-generation = "0.1"
// NOT available in JavaScript/TypeScript
// @solana/zk-sdk ❌
// @solana/confidential-transfer-proofs ❌
These libraries handle complex cryptographic operations:
Twisted ElGamal encryption
Pedersen commitments
Sigma protocol proofs
Range proofs
Equality proofs
There is no TypeScript equivalent. That already is a challenge we have to navigate to achieve true privacy.
The Trade-Offs I Made
At the moment I am just demonstrating the concept, not the cryptography. Over the next couple of days I will build a backend to handle actual zk proofs and true privacy guarantees. Another option would be to build an onchain verification program but for the sake of simplicity, I’ll avoid going down that rabbit hole and just use axum instead.
The Path to Production
Option 1: Rust Backend
Build a Rust API server that handles cryptography:
// Axum/Actix API server
#[post("/create-ct-account")]
async fn create_account(user: PublicKey) -> Json<Response> {
let elgamal = ElGamalKeypair::new_from_signer(...);
let aes_key = AeKey::new_from_signer(...);
let proof = PubkeyValidityProofData::new(&elgamal)?;
configure_account(...)?;
Json(Response { success: true })
}
The pros of this option is that I get full confidential transfer support, proper ZK proof generation with a robust system to handle all CT operations. The Cons? Well, I get to write a whole lot of idiomatic rust which is kinda a “pro”. Also the backend becomes a single point of failure which isn’t ideal.
Option 2: Anchor Program
Build on-chain verification:
#[program]
pub mod private_pass {
pub fn verify_proof(
ctx: Context<VerifyProof>,
proof: Vec<u8>,
) -> Result<()> {
// Verify ZK proof on-chain
// Grant access if valid
}
}
This might be the best option because it is fully decentralized and composable with other programs but it is quite complex to implement and could take longer. Also gas costs could be astronomical for verification.
It would also be nice if wallets added integration support for CT. Would make all these much easier.
Try It Yourself
Quick Start
# Clone the repo
https://github.com/Princeadxisrael/PrivyPass/tree/main
cd private-pass
# Install dependencies
npm install
# Create the token (one-time setup)
npm run setup:token
# Start the dev server
npm run dev
Visit
http://localhost:3000
and connect your Phantom wallet (on Devnet).
What I plan to do next?
Building the Rust backend for proper CT and Zk proof auth is priority. I should also add transaction history dashboard (maybe also show token transfers), implement token burning. I should also implement multiple access tiers.
Fun Challenges For You (Developer Reading This)
Intermediate Level:
Implement multi-tier access (Bronze/Silver/Gold)
Add NFT-based access alongside tokens
Build admin dashboard for analytics
Advanced Level:
Build the Rust backend for real CT
Create Anchor program for on-chain verification
Implement full deposit→apply→transfer→withdraw flow
Conclusion
Building Private Pass taught me that Solana’s Confidential Transfer extension is powerful, but it’s not easy to use yet. A lot of privacy discussions frame the problem as:
“Do we have the right primitives?”
Solana largely does.
The harder question I think is:
“Do developers know where those primitives belong in an application?”
And it has been made clear that privacy-aware apps are naturally multi-language, frontend-only stacks don’t scale to cryptographic responsibility and clean boundaries matter more than clever abstractions. This is a reality of building privacy systems. And the ecosystem benefits when these boundaries are acknowledged early, rather than papered over with leaky abstractions.
But herein also lies exactly the opportunity.
The first teams to build production-ready confidential access tooling (Rust SDK, easy-to-use APIs, great docs) will unlock a new category of Web3 applications:
Private DAOs
Confidential voting
Anonymous donations
Private loyalty programs
Stealth addresses for DeFi
I’ve demonstrated the concept. Now it’s time to build the real thing.
Resources
Code & Demo:
Solana Documentation:
Learn More:
Want to contribute? We’re open source! PRs welcome for:
UI improvements
Additional features
Better documentation
Rust backend implementation
If you have got questions? Drop them in the comments or reach out on Twitter.
Tags: #Solana #Web3 #Privacy #ZeroKnowledge #Token2022 #DeveloperContent #Tutorial #OpenSource

