Skip to main content

Iframe widget

Embed the swap form inside your own modal. User stays on your UI, sees your brand around ours.

<iframe
src={`https://app.zksdk.io/embed/pools/${POOL_ID}?` + new URLSearchParams({
chain: "evm",
tokenIn: userSelectedTokenIn,
tokenOut: userSelectedTokenOut,
amount: userAmountInUnits,
})}
width="420"
height="640"
style={{ border: 0 }}
/>;

useEffect(() => {
const handler = (e: MessageEvent) => {
if (e.data?.type === "PRIVATE_SWAP_COMPLETE") {
onSwapLanded(e.data.txHash);
}
};
window.addEventListener("message", handler);
return () => window.removeEventListener("message", handler);
}, [onSwapLanded]);

On success the iframe emits:

{
"type": "PRIVATE_SWAP_COMPLETE",
"txHash": "0x…",
"changeNoteCommitment": "0x…"
}

The widget emits PRIVATE_SWAP_COMPLETE on success — that's the only message you need to wire. For user-cancel (they close the modal) or error (stalled flow), handle it like any third-party iframe: give users a close button on your modal, and set a timeout on the listener (we suggest 5 minutes, which covers Groth16 proof generation on slower devices).

Validate the origin. In production, reject message events that don't come from the iframe's own origin — otherwise any page-level iframe or extension can forge a PRIVATE_SWAP_COMPLETE:

if (e.origin !== "https://app.zksdk.io") return;

What happens inside the iframe

  1. Loads pool metadata from our REST API (/api/pools/:id) and the user's shielded notes from their browser (never sent to us). Reads the pool's allowlist + token metadata directly on-chain.
  2. If the user has no shielded notes → inline Shield to Start deposit, then auto-flips to swap.
  3. Pulls the merkle tree leaves from /api/pools/:id/zk/commitments (public data cached from on-chain events); the browser builds the witness and generates a Groth16 proof locally using the user's note secrets — the secrets never leave the device.
  4. User signs one EIP-712 BroadcasterRequest. No EOA gas.
  5. Broadcaster relays through RelayAdaptShieldedPool. Pool verifies the proof, nullifies input notes, emits the change-note commitment.
  6. Iframe postMessages PRIVATE_SWAP_COMPLETE once confirmed.

Where data lives

Useful mental model.

Our REST API only holds public on-chain data (cached from the pool's own events, for speed). Your users' private data never touches our servers:

PieceWho holds itPrivate or public?
Pool config (verifier, allowlist, fees)Our REST API — cache of chain statePublic
Commitment list + nullifiers (the merkle tree's leaves)Our REST API — cache of ShieldedPool events; the chain is the source of truthPublic. A commitment is a hash — knowing it reveals nothing about the note behind it.
Token metadata, on-chain readsDirectly on-chain via viemPublic
Your users' private data (their balances, amounts, spend keys, note secrets)Their browser only — not our servers, not yours eitherPrivate
Proof generationUser's browserPrivate witness → public proof
Tx submission + gasOur broadcaster (meta-tx, any-token fee)Public

Why this is private. A commitment is a hash of (amount, token, owner, randomness) — it reveals nothing about the note behind it. We and any blockchain indexer can serve the commitment list; holding it tells you nothing about users' balances or activity. Privacy is enforced because only your user holds the preimage. In short: our API mirrors public chain events — not your users' balance data.

Ships in v0.95.