Start here
Quickstart
Go from an empty project to a real private balance in about ten minutes, on mainnet or on the free testnet. Every step below runs: no mocks, real Groth16 proofs.
- Time
- ~10 minutes
- Runtime
- browser or node
- Chain
- RH mainnet 4663, or testnet 46630
- You end with
- a spendable private note
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, such as Next or Vite). The SDK ships TypeScript source, transpiled by your bundler.
- For mainnet: USDG on Robinhood Chain (chain id
4663) plus a little ETH for gas, or USDC.e, pathUSD, USDT0 or OUSD on Tempo (4217), where gas is paid in stablecoins. Beta limits cap each stablecoin deposit at 500. See Networks. - To practice first: a wallet on Robinhood Chain testnet (
46630) with test ETH. See practice on testnet. - The circuit artifacts for your chain (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
Every deposit and spend needs a proof, so you need each circuit's wasm and zkey. The keys depend on the network: mainnet uses the final keys from the 13-contributor ceremony, the testnets use development keys, and a proof made with the wrong set does not verify. circuitArtifactUrls(circuit, chainId) returns the right { wasm, zkey } for a chain, served from https://www.gloam.trade/circuits (mainnet keys live under /circuits/final/).
import { circuitArtifactUrls } from "@gloamtrade/sdk";
circuitArtifactUrls("shield", 4663); // final keys, Robinhood Chain mainnet
circuitArtifactUrls("shield", 46630); // dev keys, Robinhood Chain testnetBase Sepolia (84532) is the exception among the testnets: it runs the mainnet vault, so its proofs use the final keys too. SDK 0.3.0 knows this, and has Base Sepolia's pool.
You can also host the files yourself and pass them to artifactProver({ wasm, zkey }): file paths in node, URLs in the browser. Just keep mainnet on the final keys.
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, so persist it. On mainnet the Robinhood Chain pool takes USDG and $GLOAM, not native ETH, so you approve the token first.
import {
mainnetNetwork, buildShieldBoundIntent, artifactProver,
circuitArtifactUrls, RH_MAINNET_USDG, gloamReadTransport,
} from "@gloamtrade/sdk";
import { createWalletClient, createPublicClient, http, parseUnits, erc20Abi } from "viem";
import { privateKeyToAccount } from "viem/accounts";
// Robinhood Chain mainnet. null until the pool is published.
const net = mainnetNetwork("robinhood");
if (!net) throw new Error("Robinhood Chain mainnet pool not published yet");
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
const wallet = createWalletClient({ account, chain: net.chain, transport: http(net.rpcUrl) });
// reads at the chain head, old logs from the network's full-history RPC where it names one
const publicClient = createPublicClient({ chain: net.chain, transport: gloamReadTransport(net) });
const amount = parseUnits("25", 6); // 25 USDG (6 decimals); beta cap is 500 per deposit
// mint the note + generate the shield proof with this chain's keys
const intent = await buildShieldBoundIntent({
chainId: net.chainId,
poolAddress: net.pool,
asset: RH_MAINNET_USDG,
amountWei: amount,
prover: artifactProver(circuitArtifactUrls("shield", net.chainId)),
});
// the pool pulls USDG with transferFrom, so approve it first
const approval = await wallet.writeContract({
address: RH_MAINNET_USDG, abi: erc20Abi, functionName: "approve", args: [net.pool, amount],
});
await publicClient.waitForTransactionReceipt({ hash: approval });
// 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, // 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;Practice on testnet with the same code. Use GLOAM_NETWORKS.robinhood (Robinhood Chain testnet, 46630) as net, and either shield test ETH (leave out asset, skip the approve, and the deposit goes in valueWei) or test USDG (RH_USDG). circuitArtifactUrls hands you the dev keys there. On Tempo, use mainnetNetwork("tempo") with TEMPO_USDCE, or GLOAM_NETWORKS.tempo to practice. On Base Sepolia, use testnetNetwork("base") (SDK 0.3.0) with BASE_SEPOLIA_USDC; it runs the mainnet vault and keys with test funds. mainnetNetwork("base") returns null until Base mainnet's pool is published, then the same code runs there with BASE_USDC. Ethereum is next the same way: mainnetNetwork("ethereum") (SDK 0.4.0) returns null until its pool is published, then runs with ETHEREUM_USDC. For USDT on Ethereum, send the approvals approvalSteps gives you: Tether's token clears an old approval to zero first.
4. Confirm it landed
The deposit inserts your commitment as a leaf in the pool's Merkle tree. Check membership directly, with no index and 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. On mainnet a cash out costs 0.25%: cashOutFee(amount) is what the pool keeps.
import {
buildUnshieldIntent, artifactProver, circuitArtifactUrls, syncTree, cashOutFee,
} from "@gloamtrade/sdk";
// rebuild the pool tree, then get this note's membership path by commitment
const synced = await syncTree(publicClient, { pool: net.pool, fromBlock: net.deployBlock });
const path = await synced.pathForCommitment(intent.note.commitment);
if (!path) throw new Error("note not found in the tree yet");
const exit = await buildUnshieldIntent({
chainId: net.chainId,
poolAddress: net.pool,
asset: RH_MAINNET_USDG,
secretHex: intent.note.secret,
amountWei: amount,
to: account.address,
path,
prove: artifactProver(circuitArtifactUrls("unshield", net.chainId)),
});
// mainnet: the pool keeps cashOutFee(amount), 0.25%, and pays the rest to "to"
console.log("fee", cashOutFee(amount));
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 network helpers, 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
Mainnet is beta software. It runs on the final keys from the 13-contributor ceremony, under beta deposit limits, after an internal review; an external audit is planned. The testnets run on dev-ceremony keys with test funds. Everything here is real: real proofs, real on-chain state, no mocks. Private swaps run in the app on both mainnets (private swaps); the SDK does not build them yet. The old sealed trade is retired. See the production gate.