Build on Gloam
API reference
The complete @gloamtrade/sdk surface. Everything exports from one barrel; the crypto core is pure and runs anywhere, and proving and storage are injected so the same code serves a browser app and a node agent.
- Package
- @gloamtrade/sdk
- Import
- one barrel
- Core
- pure, isomorphic
- Injected
- prover, storage
Import
One entry point. Tree-shake what you do not use. viem types (Address, Hex) flow through the surface; snarkjs is a lazy peer, imported only inside proveGroth16.
import {
buildShieldBoundIntent, buildUnshieldIntent, buildPrivateSendIntent,
makeBoundNotePoseidon, artifactProver,
syncTree, IncrementalMerkleTreePoseidon,
mainnetNetwork, gloamNetworks, circuitArtifactUrls, cashOutFee,
SEALED_VAULT, NATIVE_ASSET,
} from "@gloamtrade/sdk";Networks and mainnet
New in 0.1.0, and additive: every testnet export works as before. A GloamNetwork carries key, label, chainId, pool and deployBlock, and now also testnet, ceremony ("dev" | "production"), rpcUrl, explorerUrl, chain (a viem Chain) and assets (symbol, address, decimals and the beta depositLimit).
| Export | What it gives you |
|---|---|
mainnetNetwork(chain) | chain is "robinhood", "tempo", "base" (0.3.0) or "ethereum" (0.4.0). A mainnet GloamNetwork, or null until its pool is published |
testnetNetwork(chain) | 0.3.0. A chain's testnet: the Robinhood Chain and Tempo testnets, or Base Sepolia, which runs the mainnet pool and keys. 0.4.0 adds "ethereum", Ethereum Sepolia, with its pool |
gloamNetworks() | Every network Gloam runs on right now: mainnets with a pool first, then the testnets |
getGloamNetwork(key) | One network by key: "robinhood-mainnet", "tempo-mainnet", "base-mainnet", "ethereum-mainnet", "robinhood", "tempo", "base" or "ethereum"; null while its pool is not in |
GLOAM_NETWORKS | .robinhood and .tempo stay the testnets, unchanged for older code |
MAINNET_POOLS | The one place each mainnet pool and deploy block is set; null until published |
ceremonyForChainId(chainId) | Which keys a chain's verifiers hold: "production" (the final keys) on mainnet, Base Sepolia and Ethereum Sepolia, "dev" on the Robinhood Chain and Tempo testnets |
circuitArtifactUrls(circuit, chainId) | { wasm, zkey } URLs on https://www.gloam.trade/circuits for that chain's keys (mainnet under /circuits/final/). Mainnet proofs must use the final keys |
CASH_OUT_FEE_BPS / cashOutFee(amount) | 25 (0.25%) / what the pool keeps on a mainnet cash out |
RH_MAINNET_CHAIN_ID / TEMPO_MAINNET_CHAIN_ID | 4663 / 4217 |
RH_TESTNET_CHAIN_ID / TEMPO_TESTNET_CHAIN_ID | 46630 / 42431 |
RH_MAINNET_USDG, GLOAM_TOKEN | Robinhood Chain mainnet pool assets (native ETH is not one) |
TEMPO_USDCE, TEMPO_PATHUSD, TEMPO_USDT0, TEMPO_OUSD | Tempo mainnet pool assets, 6 decimals each |
BASE_USDC, BASE_EURC, BASE_USDT; BASE_SEPOLIA_USDC, BASE_SEPOLIA_EURC | 0.3.0. Base mainnet and Base Sepolia pool assets, 6 decimals each; native ETH is listed on both |
ETHEREUM_USDC, ETHEREUM_USDT, ETHEREUM_USDG, ETHEREUM_PYUSD, ETHEREUM_EURC | 0.4.0. Ethereum mainnet pool assets, 6 decimals each; native ETH is listed too |
approvalSteps(chainId, token, allowance, amount) | 0.4.0. The exact approve(pool, value) calls a deposit needs, in order: none when the allowance covers it, [0, amount] for USDT on Ethereum with an old allowance in the way, else [amount] |
ETHEREUM_SEPOLIA_USDC, ETHEREUM_SEPOLIA_USDG, ETHEREUM_SEPOLIA_PYUSD, ETHEREUM_SEPOLIA_EURC | 0.4.0. Ethereum Sepolia pool assets, 6 decimals each; native ETH is listed too |
RH_USDG | Test USDG on Robinhood Chain testnet |
MAINNET_POSEIDON_T3 | The Poseidon library both mainnet pools use |
const net = mainnetNetwork("robinhood"); // null until the pool is published
if (!net) throw new Error("not published yet");
net.testnet; // false: mainnet
net.ceremony; // "production"
circuitArtifactUrls("unshield", net.chainId); // the final keys
cashOutFee(500_000_000n); // what the pool keeps on a 500 USDG cash outIntent builders
The primary surface. Each builder returns an unsigned GloamIntent: a portable plan plus a resolved exec (the on-chain call). You sign and broadcast; the SDK never holds a key.
| Function | Signature | Status |
|---|---|---|
buildShieldBoundIntent | (p: ShieldBoundIntentParams) => Promise<ShieldIntent>Mints a note + shield proof → shieldBound(...) | Live |
buildShieldIntent | (p: ShieldIntentParams) => Promise<ShieldIntent>Plain shield(...), no proof | Legacy pools only |
buildUnshieldIntent | (p: UnshieldIntentParams) => Promise<GloamIntent<"unshield">>Witness + proof → unshield(...) (cash out) | Live |
buildPrivateSendIntent | (p: PrivateSendIntentParams) => Promise<PrivateSendIntent>Transfer witness → transfer(...); returns payment + change notes | Live |
buildPrivateTradeIntent | (p: PrivateTradeIntentParams) => Promise<PrivateTradeIntent>Sealed-swap witness → sealedSwap(...) | Retired |
buildPrivateTradeIntent produces a valid intent for the sealed trade, which is retired: the on-chain sealed-swap path stays disabled on testnet, and the mainnet pools have no sealed swap at all. Private swaps replaced it. They run in the Gloam app through a swap router on each mainnet, and the SDK does not build them yet.
Parameters
interface ShieldIntentParams {
amountWei: bigint; // wei (native) or token base units
asset?: Address; // default NATIVE_ASSET (zero address, testnet only)
chainId?: number; // default RH_TESTNET_CHAIN_ID; pass a mainnet one explicitly
poolAddress?: Address; // default: the pool for chainId (refused for a mainnet
// chain whose pool is not in the SDK yet)
agentAddress?: Address | null;
}
interface ShieldBoundIntentParams extends ShieldIntentParams {
prover: Prover; // bound to the shield circuit artifacts
}
interface UnshieldIntentParams {
secretHex: `0x${string}`; // the note's spend key
amountWei: bigint;
to: Address; // public recipient
asset?: Address;
path: PoseidonMerklePath; // membership path (from the rebuilt tree)
prove: Prover;
poolAddress?: Address; chainId?: number; agentAddress?: Address | null;
}
interface PrivateSendIntentParams {
secretHex: `0x${string}`;
amountInWei: bigint; // the source note's amount
amountPayWei: bigint; // amount to send (remainder is your change note)
asset?: Address;
path: PoseidonMerklePath;
prove: Prover;
poolAddress?: Address; chainId?: number; agentAddress?: Address | null;
}Intent shape
Every builder returns this. The plan is safe to log or hand to an agent; the exec is the resolved call.
interface GloamIntent<K> {
intent: K; // one of the four IntentKind values
chainId: number;
agentAddress: Address | null;
plan: PlanFor<K>; // portable: symbol, amount, no secrets
privacy: string; // plain-language: what is and isn't hidden
execution: string; // plain-language: how to sign + broadcast
exec: IntentExec;
}
interface IntentExec {
poolAddress: Address;
fn: PoolFn; // e.g. "shieldBound", "unshield", "transfer"
valueWei: bigint; // ETH to attach (native shield only)
args: readonly unknown[]; // ABI-ordered call args
}ShieldIntent adds note: BoundNote; PrivateSendIntent and PrivateTradeIntent add paymentNote/outNote and changeNote (each a NoteExport).
Pinned public inputs
PUBLIC_INPUTS pins the exact signal ordering the deployed verifiers expect, so an integrator cannot drift out of proof compatibility.
PUBLIC_INPUTS.unshield // [root, nullifier, asset, amount, to]
PUBLIC_INPUTS.transfer // [root, nullifier, newCommitment0, newCommitment1]
PUBLIC_INPUTS.sealedSwap // [root, nullifier, newCOut, newCChange,
// assetIn, assetOut, amountOutMin, rateIn, rateOut]Notes
A private balance is a note: commitment = Poseidon(secret, amount, asset), nullifier = Poseidon(secret, commitment). The secret is the sole spend authority.
makeBoundNotePoseidon(amount: bigint, asset?: Address): Promise<BoundNote>
noteCommitmentPoseidon(secret: bigint, amount: bigint, asset?: Address): Promise<bigint>
noteNullifierPoseidon(secret: bigint, commitment: bigint): Promise<bigint>
openNotePoseidon(secretHex: Hex, amount: bigint, asset: Address)
: Promise<{ commitment: Hex; nullifier: Hex }>
randomSecretField(): Promise<bigint> // uniform, under the field prime
interface BoundNote {
secret: Hex; commitment: Hex; nullifier: Hex;
secretField: bigint; commitmentField: bigint; nullifierField: bigint;
}Prover
Proving is environment-bound, so it is injected. A Prover takes a circom input and returns packed proof bytes. artifactProver binds snarkjs to a fixed set of artifacts (file paths in node, URLs in the browser).
type Prover = (input: Record<string, string | string[]>) => Promise<ProveResult>
type ProveResult = { proofBytes: Hex; publicSignals?: string[] }
interface Groth16Artifacts { wasm: string; zkey: string }
artifactProver(a: Groth16Artifacts): Prover
proveGroth16(input, wasm: string, zkey: string)
: Promise<{ proofBytes: Hex; publicSignals: string[]; proof: Groth16Proof }>snarkjs is imported dynamically inside proveGroth16; install it as a peer where you prove. A browser app and a node signer differ only in whether wasm/zkey are URLs or paths.
Merkle tree
A depth-20 (MERKLE_DEPTH) incremental Poseidon tree. Rebuild it from the pool's Shielded events, then produce a membership path for a spend proof.
class IncrementalMerkleTreePoseidon {
leaves: bigint[];
insert(leaf: bigint): Promise<number> // returns the leaf index
path(leafIndex: number): Promise<PoseidonMerklePath>
}
type PoseidonMerklePath = {
leafIndex: number; leaf: bigint;
pathElements: bigint[]; pathIndices: number[]; root: bigint;
}
pathToCircomInput(path: PoseidonMerklePath) // -> circom-ready string fieldsTree sync
A spend needs a membership path, which needs the current tree. syncTree rebuilds it from chain, replaying Shielded, Transferred, and SealedSwapped leaves in on-chain order, so the root stays correct after any transfer or swap. viem's PublicClient is injected, so the same call works in the browser and in node.
syncTree(client: PublicClient, { pool, fromBlock?, chunkSize? }): Promise<SyncedTree>
// true if the rebuilt root equals the pool's on-chain currentRoot()
assertTreeMatchesChain(client, pool, synced): Promise<boolean>
interface SyncedTree {
tree: IncrementalMerkleTreePoseidon;
leaves: ChainLeaf[];
root: Hex; leafCount: number;
indexByCommitment: Map<string, number>;
pathForLeaf(i: number): Promise<PoseidonMerklePath | null>;
pathForCommitment(c: Hex): Promise<PoseidonMerklePath | null>; // the ergonomic path
}The usual flow: const synced = await syncTree(client, { pool }), then hand await synced.pathForCommitment(note.commitment) straight to buildUnshieldIntent or buildPrivateSendIntent.
x402 payments
Private agent payments over HTTP 402, settle-then-prove and self-custodial. The seller prices a resource, the agent settles a private send to the payee itself, then presents the payment note and settlement tx. The public sees only a shielded transfer, never the amount or the parties. This is not a Tempo Zone: no operator sees the payment, and compliance visibility is opt-in per payment through an issuer-scoped disclosure.
// server, once: a receive key (share the tag, keep privateJwk)
generateReceiveKey(): Promise<ReceiveKey> // { tag: "gloamr1.…", privateJwk }
isReceiveTag(s) / assertReceiveTag(s)
// server: the 402 challenge (payTo must be a receive tag)
buildGloamPaymentRequirements(p): GloamPaymentRequirements
// agent: build the private send + X-PAYMENT header (does not broadcast)
buildGloamPayment(p): Promise<BuiltPayment>
// p: { requirements, senderSecretHex, senderNoteAmountWei, path, prove,
// issuerTag?, disclosureProver?, legacyUnsealed? }
// the payment note is sealed to requirements.payTo
// server: open + verify + sweep; serve only when grantAccess is true
settleGloamPayment(p): Promise<SettleResult>
// p: { requirements, payload, receiveKey, prove, chain, submit, beforeSubmit? }
// SettleResult: { grantAccess, final, status, freshNote, reason }
// lower level
openGloamPaymentNote(paymentNote, receiveKey) // open the sealed note
verifyGloamPayment({ requirements, payload, note }): VerifyResult // always final: false
sweepReceivedNote(p) // move a received note to a fresh one
verifyPaymentNoteBinding(note): Promise<boolean>
// optional issuer-scoped compliance disclosure over the payment note
buildComplianceDisclosure(p): Promise<GloamComplianceDisclosure>
encodeRequirements / decodeRequirements // 402 body transport
encodePaymentHeader / decodePaymentHeader // X-PAYMENT transport
GLOAM_VS_ZONE, GLOAM_X402_SCHEMEThe agent signs and broadcasts payment.intent.exec itself, then sets payload.txHash. The payment note travels sealed to the payee's receive tag, so nobody who sees the header can open it. The payer made that note, so it knows the secret too: a payment is only final once the payee has swept it into a note only the payee knows. settleGloamPayment opens, checks and sweeps, and returns grantAccess: true only after the sweep confirms. If the payer spent the note back first, the sweep fails and access is refused. Worked example: examples/pay-x402.
Witness builders
Pure circom-input builders under the intent builders. Use these directly only if you drive proving yourself; otherwise the intent builders call them for you.
buildPoseidonUnshieldWitness({ secretHex, amount, asset?, to, path })
buildTransferWitness({ secretHex, amountIn, amountPay, asset?, path })
buildSealedSwapWitness({ secretHex, amountIn, amountSwap, assetIn?, assetOut,
amountOutMin, rateIn, rateOut, path })
sealedSwapArtifactsReady(): boolean
type NoteExport = { secret: Hex; commitment: Hex; nullifier: Hex;
amountWei: string; asset: Address }Each returns the circom input, the derived public inputs, the output notes, and a blocker string if the witness is not provable (for example a commitment that is not in the tree).
Sealed-rate math
Pure integer math for sealed swaps, proven against the circuit equality amountOut · rateOut === amountSwap · rateIn. Rates are USD cents (SEALED_RATE_SCALE = 100) to keep the constraint exact.
marksToSealedRates(ethUsd, outUsd, source?): SealedRateQuote | null
estimateSealedOut(amountSwap: bigint, rateIn: bigint, rateOut: bigint): bigint
exactSealedAmounts(amountSwapWanted: bigint, rateIn: bigint, rateOut: bigint)
: { amountSwap: bigint; amountOut: bigint } | null // exact, no rounding leak
formatSealedAmount(wei: bigint, maxFrac?: number): string
fallbackOneToOneRates(): SealedRateQuote
type SealedRateQuote = { rateIn: bigint; rateOut: bigint; /* … */ }Size-privacy policy
The public floor an integrator publishes for a spend. The default mode keeps the on-chain minimum at SIZE_PRIVACY_OUT_MIN = 1 so the settled size leaks nothing; a slippage mode trades some privacy for a tighter guarantee.
type SizePrivacyMode = "max" | "slippage"
publicAmountOutMin(expectedOut: bigint, mode: SizePrivacyMode, bps?: number): bigint
coarsenMarkUsd(usd: number): number // bucket a mark so quotes don't fingerprint
SEALED_PRIVACY_FACTS // plain-language privacy notesField math & proof packing
Low-level helpers, shared by the note and witness layers.
// poseidon.ts
poseidon2(a: bigint, b: bigint): Promise<bigint>
poseidon3(a: bigint, b: bigint, c: bigint): Promise<bigint>
toField(x: bigint | string | number): bigint // reduce mod the field prime
fieldToHex(f: bigint): `0x${string}`
hexToField(hex: string): bigint
// proof.ts
packGroth16Proof(proof: Groth16Proof): Hex // -> the bytes the verifier wants
fieldToBytes32(field: bigint | string): HexConstants
| Constant | Value / meaning |
|---|---|
SEALED_VAULT | The hardened C1/C2/C3 pool on Robinhood Chain testnet, the default deposit target when no chain is given |
SHIELD_VERIFIER | The deployed shield verifier the hardened pool checks against |
GLOAM_PAY_MEMO | The encrypted-memo contract for private-pay discovery |
NATIVE_ASSET | Zero address, meaning native ETH (a pool asset on testnet only) |
RH_TESTNET_CHAIN_ID / RH_MAINNET_CHAIN_ID | 46630 / 4663 |
TEMPO_TESTNET_CHAIN_ID / TEMPO_MAINNET_CHAIN_ID | 42431 / 4217 |
FIELD_PRIME | The BN254 scalar field modulus |
MERKLE_DEPTH / SEALED_RATE_SCALE | 20 / 100 |
The default SEALED_VAULT is the hardened pool, never the retired 0x4F38 deployment. Pass chainId (and optionally poolAddress) to any builder to target another network. Mainnet pools come from MAINNET_POOLS; see Networks and mainnet.
Guardrails
Mainnet (4663, 4217, 8453, 1) is beta software, on the final ceremony keys, with beta deposit limits and a 0.25% cash-out fee. 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 proofs only, never a mock. Note secrets are the sole spend authority: persist and protect them. See the quickstart to put this together, or the production gate for what is done and what is still ahead.