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.
| 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(...) | 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 fieldsTree 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_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, the default deposit target |
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 |
RH_TESTNET_CHAIN_ID / RH_MAINNET_CHAIN_ID | 46630 / 4663 |
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 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.