Documentation
Quickstart
Go from an empty project to a real private balance on Robinhood Chain testnet in under ten minutes. Every step below runs — no mocks, real Groth16 proofs.
Fastest start
Scaffold a working private app in one command:
npm create gloam-app@latest my-private-app
cd my-private-app && npm install && npm run devThat gives you the browser shield flow, wired and running on localhost:3000. The rest of this page builds the same thing by hand so you understand each piece.
What you need
- Node 18+ (or a browser app with a bundler — Next, Vite). The SDK ships TypeScript source, transpiled by your bundler.
- A funded testnet account. Point a wallet at Robinhood Chain testnet (chain id
46630) and get test ETH — see the testnet guide. - The shield circuit artifacts,
shield.wasmandshield_final.zkey(step 2).
1. Install
viem handles signing and RPC; snarkjs is the proving peer, loaded only when you generate a proof.
npm install @gloamtrade/sdk viem
npm install snarkjs # peer, for proving2. Get the circuit artifacts
Shielding into the hardened pool requires a proof, so you need the shield circuit's wasm and zkey. The reference app serves them from app/public/circuits/:
shield.wasm— the witness generatorshield_final.zkey— the proving keyshield_vkey.json— the verifying key (only for disclosure verification)
In node, pass file paths. In the browser, host the files and pass URLs (for example /circuits/shield.wasm). Same artifacts either way.
3. Shield your first note
buildShieldBoundIntent does the private half: it mints a note (a Poseidon commitment binding your secret to the amount and asset) and generates the shield proof. You sign the resolved call. The note.secret it returns is your only spend authority — persist it.
import { buildShieldBoundIntent, artifactProver } from "@gloamtrade/sdk";
import { createWalletClient, http, parseEther, defineChain } from "viem";
import { privateKeyToAccount } from "viem/accounts";
const rhTestnet = defineChain({
id: 46630,
name: "Robinhood Chain testnet",
nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
rpcUrls: { default: { http: ["<RH_TESTNET_RPC>"] } },
});
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
const wallet = createWalletClient({ account, chain: rhTestnet, transport: http() });
// mint the note + generate the shield proof
const intent = await buildShieldBoundIntent({
amountWei: parseEther("0.001"),
prover: artifactProver({
wasm: "./shield.wasm", // URL in the browser
zkey: "./shield_final.zkey",
}),
});
// sign + broadcast shieldBound(asset, amount, commitment, proof)
const hash = await wallet.writeContract({
address: intent.exec.poolAddress,
abi: SHIELD_BOUND_ABI,
functionName: intent.exec.fn, // "shieldBound"
args: intent.exec.args,
value: intent.exec.valueWei, // the deposit for native ETH, 0n for tokens
});
// PERSIST THIS. Losing it loses the funds.
saveSecret(intent.note.commitment, intent.note.secret);The minimal ABI fragment for the one call you make:
const SHIELD_BOUND_ABI = [{
type: "function",
name: "shieldBound",
stateMutability: "payable",
inputs: [
{ name: "asset", type: "address" },
{ name: "amount", type: "uint256" },
{ name: "commitment", type: "bytes32" },
{ name: "proof", type: "bytes" },
],
outputs: [],
}] as const;4. Confirm it landed
The deposit inserts your commitment as a leaf in the pool's Merkle tree. Check membership directly — no index, no backend:
const seen = await publicClient.readContract({
address: intent.exec.poolAddress,
abi: [{ type: "function", name: "commitmentSeen", stateMutability: "view",
inputs: [{ name: "c", type: "bytes32" }], outputs: [{ type: "bool" }] }],
functionName: "commitmentSeen",
args: [intent.note.commitment],
});
// seen === true -> you hold a private balanceThat is a real shielded balance. Nobody can tell it is yours or spend it without note.secret.
5. Cash out
To exit, rebuild the tree from chain, prove membership, and unshield. syncTree replays every leaf-inserting event in order (so the root matches even after transfers); buildUnshieldIntent builds the witness and proof. The amount, asset, and recipient become public on exit — the source note stays unlinkable via the nullifier.
import { buildUnshieldIntent, artifactProver, syncTree, SEALED_VAULT } from "@gloamtrade/sdk";
// rebuild the pool tree, then get this note's membership path by commitment
const synced = await syncTree(publicClient, { pool: SEALED_VAULT, fromBlock });
const path = await synced.pathForCommitment(note.commitment);
if (!path) throw new Error("note not found in the tree yet");
const exit = await buildUnshieldIntent({
secretHex: note.secret,
amountWei: parseEther("0.001"),
to: account.address,
path,
prove: artifactProver({ wasm: "./unshield.wasm", zkey: "./unshield_final.zkey" }),
});
await wallet.writeContract({
address: exit.exec.poolAddress,
abi: unshieldAbi,
functionName: exit.exec.fn, // "unshield"
args: exit.exec.args,
});Where to go next
- API reference — every builder, the note and prover primitives, merkle, rates, and constants.
- Private pay — send inside the vault to a receive tag with
buildPrivateSendIntent. - Selective disclosure — prove one balance to a chosen party, revealing nothing else.
- Build a private agent — the same core, server-side, under policy.
Honesty
Robinhood Chain testnet only, with dev-ceremony proving keys. Everything here is real — real proofs, real on-chain state, no mocks. Sealed swaps are disabled pending the H1 solvency work. Mainnet waits for a production ceremony and an external audit; see the production gate.