Meet $GLOAM, the Gloam token.

See more

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

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, 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, meaning 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.