Raven Bridge: moving value across chains, gated by identity
A cross-chain bridge gated by zkPassport identity.
- Role
- Full-stack + SDK
- Year
- 2024
- Category
- Web3
- Stack
- Next.js 15, Aztec.js, @ravenhouse/omni-sdk

The problem#
Bridges are the highest-value target in crypto, and Aztec makes the design more interesting: the L2 side is private, so a naive bridge would let anyone move funds into an anonymity set with no accountability at all. Raven Bridge answers that with an identity gate: assets move between an Ethereum L1 portal and an Aztec L2 bridge contract, but only wallets that have completed zkPassport verification (a real, unique human, proven without revealing passport data) are allowed through.
The engineering problem underneath the product is that L1 and L2 do not share a clock. A deposit on L1 is not instantly spendable on L2; you deposit, wait for the message to be consumable, then claim. Getting that two-phase flow to survive page refreshes, wallet disconnects, and users walking away mid-bridge is most of the work.
My role#
I built the Next.js 15 frontend and the state model, and I did every contract call
through @ravenhouse/omni-sdk, the SDK I authored for exactly this.
The bridge is the SDK's first and most demanding consumer: if the deposit-and-claim
flow was awkward to express in the SDK, I felt it here first and fixed it there.
Architecture#
Ethereum (Sepolia) Aztec (testnet)
┌──────────────────────┐ ┌──────────────────────┐
│ USDC Portal (L1) │ L1→L2 message │ USDC Bridge (L2) │
│ deposit(amount, │──────────────────▶│ claim(secret) │
│ claimSecret) │ │ → private or public │
└──────────────────────┘ └──────────────────────┘
▲ ▲
│ omni-sdk RavenBridge client │
│ ( L1ToL2Orchestrator / L2ToL1 ) │
┌────────┴───────────────────────────────────────── ┴────────┐
│ Next.js app · Jotai state · zkPassport gate · checkpoints │
└────────────────────────────────────────────────────────────┘State lives in Jotai atoms, and the bridge writes a checkpoint the moment the L1
deposit confirms. That checkpoint carries the claim secret, which is the one piece of
data a user cannot afford to lose: without it the L2 claim cannot be reconstructed. The
SDK surfaces it through an onDepositComplete callback whose errors deliberately do not
abort the bridge, so a failed analytics write can never cost someone their funds.
The hardest decision#
Where does identity verification sit in the flow? Verifying on-chain for every bridge would be expensive and slow. Verifying purely client-side would be trivially bypassable. I settled on a backend-attested status check: the app asks the verification service whether an Aztec address has a valid zkPassport credential before it ever lets the user arm a deposit, and the service is the authority. The SDK models this as a small, honest interface:
export class IdentityVerifier {
constructor(private backendUrl: string) {}
/** Is this Aztec address a verified, unique human? */
async checkStatus(aztecAddress: string): Promise<VerificationStatus> {
const res = await fetch(
`${this.backendUrl}/verify-identity/status?address=${aztecAddress}`,
)
if (!res.ok) {
throw new Error(`Failed to check verification status: ${res.statusText}`)
}
const data = await res.json()
return {
isVerified: data.isVerified ?? false,
zkPassportId: data.zk_passport_id,
verifiedAt: data.verified_at ? new Date(data.verified_at) : undefined,
}
}
}The code#
Bridging itself reduces to a single typed call. The isPrivate flag decides whether the
claimed funds land in a private note or a public balance on L2, and the human-memorable
secret is hashed into the field that derives the claim secret, so a user can recover a
claim from a phrase instead of a 32-byte blob:
const bridge = new RavenBridge({ network })
const result = await bridge.bridgeL1ToL2({
l1Wallet, // wagmi / viem compatible
l2Wallet, // Azguard or an Aztec AccountWallet
token: USDC,
amount: "100.5",
isPrivate: true, // claim into a private note on L2
secret: userPhrase, // derives the recoverable claim secret
onStep: (step) => tracker.set(step),
onDepositComplete: async (checkpoint) => persist(checkpoint),
})Outcome#
Raven Bridge is live at bridge.ravenhouse.xyz. The L1 USDC portal on Sepolia is on Etherscan and the L2 bridge is on AztecScan. Everything the bridge does, it does through the SDK, which meant the SDK had to be good. That is the next case study.