Private stocks are live. Swap TSLA, NVDA, AAPL and more on Robinhood Chain from your private balance.

Swap privately

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

ExportWhat 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_POOLSThe 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_ID4663 / 4217
RH_TESTNET_CHAIN_ID / TEMPO_TESTNET_CHAIN_ID46630 / 42431
RH_MAINNET_USDG, GLOAM_TOKENRobinhood Chain mainnet pool assets (native ETH is not one)
TEMPO_USDCE, TEMPO_PATHUSD, TEMPO_USDT0, TEMPO_OUSDTempo mainnet pool assets, 6 decimals each
BASE_USDC, BASE_EURC, BASE_USDT; BASE_SEPOLIA_USDC, BASE_SEPOLIA_EURC0.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_EURC0.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_EURC0.4.0. Ethereum Sepolia pool assets, 6 decimals each; native ETH is listed too
RH_USDGTest USDG on Robinhood Chain testnet
MAINNET_POSEIDON_T3The 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 out

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(...)
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 fields

Tree 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_SCHEME

The 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 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 on Robinhood Chain testnet, the default deposit target when no chain is given
SHIELD_VERIFIERThe deployed shield verifier the hardened pool checks against
GLOAM_PAY_MEMOThe encrypted-memo contract for private-pay discovery
NATIVE_ASSETZero address, meaning native ETH (a pool asset on testnet only)
RH_TESTNET_CHAIN_ID / RH_MAINNET_CHAIN_ID46630 / 4663
TEMPO_TESTNET_CHAIN_ID / TEMPO_MAINNET_CHAIN_ID42431 / 4217
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 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.