The Gloam SDK is live. Add private balances, payments, and disclosure to any Robinhood Chain app.

Explore the SDK →

Documentation

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.

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,
  SEALED_VAULT, NATIVE_ASSET,
} from "@gloamtrade/sdk";

Intent 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.

FunctionSignatureStatus
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(...)
Disabled (H1)

buildPrivateTradeIntent produces a valid intent, but the on-chain sealed-swap path is disabled pending the H1 solvency work — the builder is here so you can wire it ahead of the flip.

Parameters

interface ShieldIntentParams {
  amountWei: bigint;          // wei (native) or token base units
  asset?: Address;            // default NATIVE_ASSET (zero address)
  chainId?: number;           // default RH_TESTNET_CHAIN_ID
  poolAddress?: Address;      // default SEALED_VAULT
  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 fields

Tree sync

A spend needs a membership path, which needs the current tree. syncTree rebuilds it from chain — replaying Shielded, Transferred, and SealedSwappedleaves 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.

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 notes

Field 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): Hex

Constants

ConstantValue / meaning
SEALED_VAULTThe hardened C1/C2/C3 pool — the default deposit target
SHIELD_VERIFIERThe deployed shield verifier the hardened pool checks against
GLOAM_PAY_MEMOThe encrypted-memo contract for private-pay discovery
NATIVE_ASSETZero address — native ETH
RH_TESTNET_CHAIN_ID / RH_MAINNET_CHAIN_ID46630 / 4663
FIELD_PRIMEThe BN254 scalar field modulus
MERKLE_DEPTH / SEALED_RATE_SCALE20 / 100

The default SEALED_VAULT is the hardened pool — never the retired 0x4F38 deployment. Pass poolAddress to any builder to override it.

Guardrails

Robinhood Chain testnet 46630 with dev-ceremony keys. 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 mainnet still needs.