Build on Gloam
SDK
The privacy layer for Robinhood Chain, Tempo, Base and Ethereum, as a package. Give any app or agent shielded balances, private payments, and selective disclosure, without building a proving stack from scratch.
- Package
- @gloamtrade/sdk
- Runtime
- browser + node
- Chains
- Mainnet 4663 · 4217 · 8453 · 1, testnet 46630 · 42431 · 84532 · 11155111
- Proofs
- Groth16 + Poseidon
What Gloam gives your app
Robinhood Chain, Tempo, Base and Ethereum are public: every balance, size, and move is visible. Gloam is the shielded chamber on top of them, and @gloamtrade/sdk is that chamber as a dependency. The Gloam vault app is the reference implementation, not a special case: the same core runs in your app.
- Shielded balances. Deposit stablecoins (and $GLOAM on Robinhood Chain) into a private balance only the holder can see or spend. The testnets also take test ETH and stock tokens.
- Private payments. Send inside the vault to a receive tag, with on-chain encrypted discovery.
- Selective disclosure. Let a holder prove one balance to a chosen party, such as an auditor or a counterparty, revealing nothing else. See the guide.
- Cash out. Unshield to a public balance with a real browser- or node-generated Groth16 proof.
Install
The SDK ships TypeScript source, consumed through your bundler (Next transpilePackages, Vite, tsx), the same way the reference app uses it. snarkjs is an optional peer, needed only when you generate proofs (shield, unshield, disclosure).
npm install @gloamtrade/sdk viem
npm install snarkjs # optional peer, for provingMainnet and testnet
From 0.1.0 the SDK knows the Robinhood Chain and Tempo mainnets and testnets; 0.3.0 adds Base and Base Sepolia, and 0.4.0 Ethereum and Ethereum Sepolia. The builders still default to Robinhood Chain testnet so older code keeps working, so pass a mainnet network explicitly.
mainnetNetwork("robinhood" | "tempo" | "base" | "ethereum")returns a mainnetGloamNetwork, ornulluntil its pool is published.gloamNetworks()lists every network Gloam runs on right now, mainnets first.getGloamNetwork(key)takes"robinhood-mainnet","tempo-mainnet","base-mainnet","ethereum-mainnet","robinhood","tempo","base"(Base Sepolia) or"ethereum"(Ethereum Sepolia).GLOAM_NETWORKS.robinhoodand.tempostay the testnets, unchanged.- Builders resolve the pool from
chainIdwhenpoolAddressis left out, and refuse a mainnet chain whose pool is not in the SDK yet. - Mainnet proofs must use the final keys from the 13-contributor ceremony.
circuitArtifactUrls(circuit, chainId)picks the right set for the chain;ceremonyForChainIdtells you which one it is. - On Robinhood Chain mainnet the pool takes USDG (
RH_MAINNET_USDG) and $GLOAM (GLOAM_TOKEN), not native ETH. On Tempo mainnet it takesTEMPO_USDCE,TEMPO_PATHUSD,TEMPO_USDT0andTEMPO_OUSD. - Beta deposit limits: 500 per deposit and 25,000 in total for each stablecoin; $GLOAM 25,000 per deposit and 250,000 in total. Each network's
assetscarry thedepositLimit. - A mainnet cash out costs 0.25% (
CASH_OUT_FEE_BPS = 25);cashOutFee(amount)is what the pool keeps. Deposits and private payments are free.
The model
Gloam is a shielded pool. A private balance is a note:
commitment = Poseidon(secret, amount, asset)
nullifier = Poseidon(secret, commitment)Shielding inserts the commitment as a leaf in a depth-20 incremental Merkle tree; the secret is the only spend authority and never leaves the client. Spending (send, cash out, trade) proves in zero knowledge that you know the secret for a commitment in the tree, and publishes the nullifier so it cannot be spent twice, without revealing which note. The chain verifies a Groth16 proof, never your identity or size. Nothing is a mock; if a path cannot be private, it waits.
Quickstart: shield privately
The hardened pool enforces a proof at deposit (audit C1), so plain shield() reverts. buildShieldBoundIntent mints the note and generates the shield proof; you sign the resolved call.
import {
mainnetNetwork, buildShieldBoundIntent, artifactProver,
circuitArtifactUrls, TEMPO_USDCE,
} from "@gloamtrade/sdk";
const net = mainnetNetwork("tempo"); // null until the pool is published
if (!net) throw new Error("Tempo mainnet pool not published yet");
const intent = await buildShieldBoundIntent({
chainId: net.chainId,
poolAddress: net.pool,
asset: TEMPO_USDCE,
amountWei: 25_000_000n, // 25 USDC.e (6 decimals); beta cap is 500 per deposit
prover: artifactProver(circuitArtifactUrls("shield", net.chainId)),
});
// approve the pool for the token first (it pulls it with transferFrom), then
// sign intent.exec: a ready shieldBound(asset, amount, commitment, proof) call.
// intent.note.secret is the spend key. PERSIST IT.
await wallet.writeContract({
address: intent.exec.poolAddress,
abi: shieldPoolAbi,
functionName: intent.exec.fn, // "shieldBound"
args: intent.exec.args,
});To practice on the free testnet, use GLOAM_NETWORKS.tempo (or GLOAM_NETWORKS.robinhood, where you can also shield test ETH) as net; the same code proves with the dev keys there. A complete runnable agent is in examples/agent-shield. See the quickstart and Build a private agent.
Intents: plan and exec
Every action is an intent with two layers. The plan is portable and safe to log or hand to an agent (symbol, amount, no secrets). The exec resolves the on-chain call (wei amounts, resolved addresses, proof args). The SDK and the agent server share one intent shape.
| Builder | Resolves |
|---|---|
buildShieldBoundIntent | Mints a note + shield proof → shieldBound(...) (the live deposit path) |
buildShieldIntent | Plain shield(...), only for pools without a shield verifier |
buildUnshieldIntent | Witness + proof → unshield(...) (cash out) |
buildPrivateSendIntent | Transfer witness → transfer(...) to a receive tag |
API surface
Notes
import { makeBoundNotePoseidon, noteNullifierPoseidon } from "@gloamtrade/sdk";
const note = await makeBoundNotePoseidon(amountWei, assetAddress);
// { secret, commitment, secretField, commitmentField }
const nullifier = await noteNullifierPoseidon(note.secretField, note.commitmentField);Prover (injected)
Proving is environment-bound, so it is passed in. artifactProver binds snarkjs to your circuit artifacts (paths in node, URLs in the browser); proveGroth16 is the one-shot form.
import { artifactProver, proveGroth16 } from "@gloamtrade/sdk";
const prover = artifactProver({ wasm, zkey });
const { proofBytes, publicSignals } = await prover(circomInput);Merkle
A depth-20 incremental tree and a circom path builder (buildPoseidonMerklePath) for spend proofs. Rebuild the tree from the pool's Shielded events, then prove membership.
Rates & privacy
Pure sealed-rate math (exactSealedAmounts) and the size-privacy floor policy (publicAmountOutMin), proven against the circuit equality amountOut·rateOut = amountSwap·rateIn.
Constants
SEALED_VAULT (the hardened Robinhood Chain testnet pool), SHIELD_VERIFIER, NATIVE_ASSET, the chain ids, and for mainnet MAINNET_POOLS, the asset addresses and MAINNET_POSEIDON_T3. The default SEALED_VAULT is the hardened C1/C2/C3 pool, never the retired 0x4F38 pool.
Integration patterns
- Private balances in your app. Shield with
buildShieldBoundIntent, persistnote.secretin your own store keyed by commitment, cash out with the unshield builder. - Private payments. Send to a receive tag; the recipient scans the encrypted
GloamPayMemoinbox and opens the note (on mainnet, until it has a board, the payer hands over the sealed code). See Private pay and Take payments and read their notes. - Compliance without opacity. A holder proves one balance to an auditor via a disclosure, revealing nothing else.
- Autonomous agents. The same core runs server-side, so an AI agent can hold and move value privately under policy. See Agents.
Take payments and read their notes
New in 0.2.0. A service can ask to be paid privately, with a note on each payment (an invoice number, an agent ID), and credit each payment by its note. Only your key reads the note, and nothing on chain ties the payer to it.
- Hold a receive key.
generateReceiveKey()gives you a Gloam address (gloamr1.…) to share and a private key to keep on your server. - Make a request link.
buildPaymentRequestLinkbuilds the same link the Gloam app makes: your address, the network, the token, an optional amount, and a note of up to 80 characters. A longer note is refused, not cut. - The payer pays it from their private balance in Gloam. The note travels inside the payment, sealed to your address.
- Find it. Where the network has a payment-message board,
scanIncomingPaymentsfinds every payment sealed to your key, with its amount, token and note. It only returns payments that are in the vault and not yet spent, read at one block. Resume each scan fromscannedTo + 1. - Secure it, then credit. Until you move it, the payer could still spend it back.
sweepReceivedNotemoves it into a note only you know, through the Gloam relay, so your server needs no gas. Credit the payment only when the sweep returnsfinal: true.
import {
mainnetNetwork, generateReceiveKey, buildPaymentRequestLink,
scanIncomingPayments, openIncomingPayment, notePackageToExport,
sweepReceivedNote, sweepChainFromClient, relaySubmitter, findRequestAsset,
} from "@gloamtrade/sdk";
const net = mainnetNetwork("robinhood")!;
const key = await generateReceiveKey(); // keep key.privateJwk secret
const usdg = findRequestAsset(net, "USDG")!; // the token you credit
const link = buildPaymentRequestLink({
payTo: key.tag, network: net, asset: "USDG", amount: "25", note: "agent:42",
});
// With a payment-message board: scan for payments to your key.
const { payments, scannedTo } = await scanIncomingPayments({ client, network: net, receiveKey: key });
// Or check a sealed code a payer hands over directly.
const check = await openIncomingPayment({ client, network: net, receiveKey: key, sealedTicket: code });
for (const p of payments) {
const r = await sweepReceivedNote({
note: await notePackageToExport(p.notePackage),
poolAddress: net.pool, chainId: net.chainId, prove,
chain: sweepChainFromClient(client, { pool: net.pool, fromBlock: net.deployBlock }),
submit: relaySubmitter(),
beforeSubmit: (fresh) => saveNote(fresh), // persist before it is sent
});
// Anyone can pay in any token the vault takes: credit only the one you priced in.
if (r.final && p.asset.toLowerCase() === usdg.address.toLowerCase()) creditAgent(p.note, p.amountWei);
}Every network has a payment-message board, Robinhood Chain and Tempo mainnet included, so scanIncomingPayments finds payments to your key on its own. A payer can also hand you the sealed code (gloam2t.…) their app shows after paying: check it with openIncomingPayment, which says whether the payment is ready, has not landed yet, or was already spent. A complete, runnable version is in packages/sdk/examples/receive-and-credit.ts.
Public inputs are pinned
The SDK pins the exact public-input ordering the deployed verifiers expect, so an integrator cannot drift out of proof compatibility:
- shield,
[commitment, amount, asset] - unshield,
[root, nullifier, asset, amount, to] - transfer,
[root, nullifier, newCommitment0, newCommitment1] - sealedSwap, nine signals ending in
amountOutMin, rateIn, rateOut
Guardrails
Mainnet (Robinhood Chain 4663, Tempo 4217, Base 8453, Ethereum 1) is beta software: final ceremony keys, beta deposit limits, a 0.25% cash-out fee. It was reviewed internally before launch; an external audit is planned. The Robinhood Chain and Tempo testnets (46630, 42431) run on dev-ceremony keys with test funds; Base Sepolia (84532) and Ethereum Sepolia (11155111) run the final keys with test funds. Real privacy only, never a mock success. Note secrets are the sole spend authority: persist and protect them. The old sealed trade is retired. Private swaps run in the app on Robinhood Chain and Tempo mainnet; the SDK does not build them yet. See the production gate.