Skip to content
09 / 12

SDK

@hushd/sdk is the TypeScript behind the app, the MCP server and the makers. It works in the browser and in Node, and you sign everything yourself.

Package
@hushd/sdk
Runs in
Browser and Node 22+
Network
NEAR testnet

Install

The SDK lives in the repo as packages/sdk and ships as TypeScript source (ES modules). Inside the monorepo, depend on it with "@hushd/sdk": "workspace:*". Node-only helpers, an encrypted note file and cached proving artifacts, are in @hushd/sdk/node.

Call loadPoseidon() once before anything that hashes: identities, notes and proofs all use it.

A trade, end to end

import {
  awaitMatch, checkMatchedTerms, DeskApi, identityFromSeed, loadPoseidon, openRfq,
  packageRequest, PrivateWallet, RelayerClient, settleAsTrader, TESTNET,
  type PackageDraft,
} from "@hushd/sdk";
import { cachedArtifacts, EncryptedFileStore } from "@hushd/sdk/node";

await loadPoseidon();
const id = identityFromSeed(seed); // 32 bytes you keep secret
const relayer = new RelayerClient(TESTNET.api);
const api = new DeskApi(TESTNET.api);
const wallet = new PrivateWallet(TESTNET, id, {
  artifacts: await cachedArtifacts({ cacheDir: ".hushd/circuits" }),
  relayer,
  store: new EncryptedFileStore(".hushd/notes", seed),
});
await wallet.restore();
await wallet.sync();

// Sell 100 USDC for wNEAR, taking the best quote.
const draft: PackageDraft = {
  kind: "spot",
  sell: { token: "usdc.hushd.testnet", amount: 100_000_000n },
  buy: { token: "wnear.hushd.testnet", minAmount: 0n },
};
// the request names a one-time address for this deal alone (dealIdentity), never yours
const { request, ask, refundBlinding, dealIdentity } = packageRequest(id, TESTNET, draft);
const { rfqId } = await openRfq({ net: TESTNET, api, relayer, request, ask, windowMs: 45_000 });
const match = await awaitMatch(api, dealIdentity, rfqId);
checkMatchedTerms(match.terms, draft); // throws unless the match is what you asked for
const { moved } = await settleAsTrader({
  net: TESTNET, api, wallet, refundBlinding, // re-derives the deal's owner key and its one-time address
  dealId: match.dealId, terms: match.terms, makerAddress: match.maker_address,
});
// moved.state "moved": the tokens are in your balance; "waiting": the next wallet.sync() moves them

The enclave picks the best quote, but you check the result before your leg is proved: checkMatchedTerms refuses a match that pays less than your minAmount, in another token, or as another package. A deal you do not settle simply expires.

For a hedged package, keep the refundBlinding that packageRequest returns: it re-derives the key that closes the hedge, the note a slashed bond pays you, and the deal's one-time address. The maker pays your leg to that address; the wallet keeps the note's opening and moves it into your balance with a private send through the relayer, trying again on each sync until it lands. The desk page in the app does exactly this, in app/src/app/app/page.tsx.

A private payment

// The deposit is yours to sign: send call to the token contract (1 yocto attached).
const { call, selfPost } = wallet.shield("usdc.hushd.testnet", 25_000_000n);
await relayer.post(selfPost, "note"); // so your seed alone can recover this note

// Later, from the private balance, through the relayer.
const { txHash } = await wallet.send("hushd1…", "usdc.hushd.testnet", 10_000_000n, "invoice 7");

To pay someone who is not on Hushd yet, pay into a one-time note and hand them a link. The link carries the note's opening after the #, which browsers never send to a server. Whoever opens it first claims the payment into a new note for their own receive key, made on their device, so after the claim you cannot spend it back. Until then you can take it back.

import { checkLink, claimLink, linkUrl, parseLink, payAsLink, sentLinks, takeBackLink, TESTNET } from "@hushd/sdk";

// The payer: the link is saved in the wallet's encrypted store before the payment goes out.
const { link } = await payAsLink(wallet, "usdc.hushd.testnet", 25_000_000n);
const url = linkUrl("https://hushd.net", link); // https://hushd.net/app/claim#…, share it privately

// The claimer: check the pool first, then claim into their own wallet. The relayer pays the gas.
const theirs = parseLink(url);
const check = await checkLink(TESTNET, theirs); // "ready", "claimed" or "missing"
if (check.state === "ready") await claimLink(theirs, { to: theirWallet, artifacts, relayer, tree: check.tree });

// Later, the payer: where each link stands, and taking back one nobody claimed.
const [first] = await sentLinks(wallet); // state: "waiting", "claimed", "taken back", …
if (first?.state === "waiting") await takeBackLink(wallet, first.link, { artifacts, relayer });

Exports

Network

ExportWhat it is for
TESTNET, MAINNETWhere Hushd lives: RPC, contracts, tokens, the API. MAINNET names the mainnet contracts and real tokens; deployed is false until they exist
withOverrides(net, partial)Point any field elsewhere: a local sandbox, a self-hosted API
tokenInfo(net, idOrSymbol)A token's account id, symbol, decimals and hedge coin
txUrl, accountUrlNearBlocks links for a transaction or an account

Identity

ExportWhat it is for
identityFromSeed(seed)The spending key, receive key, X25519 key and hushd1… address from 32 bytes
randomSeed()A fresh seed, for an agent or a test. Keep it: it is the identity
seedFromSignature(sig), SEED_MESSAGEThe seed behind a NEP-413 wallet signature of the fixed message
encodeAddress, decodeAddress, isAddresshushd1… addresses: bech32m of the receive key and the X25519 key

Private wallet

ExportWhat it is for
new PrivateWallet(net, id, { artifacts, relayer, store })The notes one identity owns, synced with the pool and the board
restore(), sync()Load the store, then pull new leaves and board posts and mark spent notes
balances(), spendable(token), pending()What it holds, what it can spend now, what is still landing
shield(token, amount)A new note, the ft_transfer_call to sign, and the self-post that makes it recoverable
send(to, token, amount, memo?)Pay a hushd1… address privately, through the relayer
pay(note, { word, beforeSend })Pay a note made beforehand, optionally sealing word of it to its owner
cashOut(account, token, amount, fee?)Pay out to a NEAR account, with a refund note if the payout fails
payments(), deals()What it paid and traded, ready for a proof
MemoryStore, artifactsAt(base)An in-memory store, and proving artifacts from a URL
ExportWhat it is for
payAsLink(wallet, token, amount)Pay into a one-time note and get a link for it; the link is in the wallet's store before the payment goes out
linkUrl(appOrigin, link), parseLink(urlOrCode)The URL to share (the code after the #), and back
encodeLink, decodeLink, newLinkThe versioned, checked code on its own; a LinkError says what is wrong with a damaged one
checkLink(net, link)ready, claimed or missing, from the pool: only a ready link's amount is real
claimLink(link, { to, artifacts, relayer })Spend the link note into a new note for to's receive key, through the relayer: no gas
sentLinks(wallet), takeBackLink(wallet, link, opts)The links a wallet paid with their state, and taking back one nobody claimed
ALREADY_CLAIMEDThe one answer for a spent link, claimed or taken back

Desk

ExportWhat it is for
packageRequest(id, net, draft)The sealed request (with the limit) and the ask makers see
openRfq({ net, api, relayer, request, ask, windowMs })Seal and commit the request, and send each maker its ask, skipping makers that let too many deals lapse
readMakerRecords(net), makersToAskEach maker's record from the desk's deals (settled, lapsed, lapse rate), and the makers a request asks
awaitMatch(api, dealIdentity, rfqId)Wait for the enclave and open the terms sealed to this deal's one-time address
checkMatchedTerms(terms, draft)Refuse a match below your limit, in another token or another package
isBondStep(bond, minBond), bondStepAtLeast(atLeast, minBond)The desk takes a bond only in steps of its minimum (1, 2, 5, 10, 20... times it)
settleAsTrader, settleAsMakerEach side's settlement, from planning to the settled deal (the trader's then moves into its balance)
deskViewconfig, enclave, makers, deal, deals, stats
new DeskApi(url), new RelayerClient(url)The API's request intake and deal rooms, and the relayer
ownerKeyFor, closeHedgeSignatureThe per-deal key that alone closes a hedge, and its signature
dealIdentityFor, wallet.moveDealNote(dealId)The one-time address a deal pays you at, and the private send that moves its note into your balance

Proofs

ExportWhat it is for
proveReceipt(src, id, scope, made, paidWith, opts)A payment, exact or at least an amount
proveFunds(src, id, scope, tree, notes, threshold)At least a threshold, in one to four notes
proveVolume(src, id, scope, legs)The total delivered across one to sixteen desk deal legs
encodeProof, decodeProofTo and from a hushdproof1… string
verifyProof(net, proof, vkey, { asReader })Check it as its reader would: off chain, then through the pool

Intake

ExportWhat it is for
intake(apiBase, text)Your words to a draft package, end-to-end encrypted, with the TEE's signature checked
intakeAttestation(apiBase)The model's verified attestation, and whether intake is on
encryptForModel, decryptFromModelThe encryption the intake uses, on its own

x402

ExportWhat it is for
paymentRequired(net, requirement)The 402 body for scheme hushd
x402Fetch(wallet, url, init, { maxAmount })Fetch, and pay a private 402 within a cap
verifyX402Payment(net, payee, header, price, seen)Check an X-PAYMENT header on the server
paymentResponse, x402Network, X402_SCHEMEThe response header, the network name, the scheme

Reads

ExportWhat it is for
poolViewroot, isKnownRoot, nextLeafIndex, leaves, isSpent, spendOf, assets, stats, config, verifyDisclosure
boardView, scanBoardBoard posts, and the ones that open for an identity
loadTree(net)The pool's Merkle tree rebuilt from its stored leaves
view, accountBalance, txStatus, latestBlockPlain NEAR RPC reads, with endpoint fallback

Lower down, the SDK also exports what these are built from: notes (makeNote, nullifierOf), the Merkle tree (MerkleTree, depth 20), Poseidon, the joinsplit prover, the pool's call arguments (shieldCall, transactRequest, swapLeg), the domain hashes (assetField, extHash, swapExtHash, proofContext) and the sealed boxes (sealJson, openJson).

Guardrails

  • The SDK never holds a NEAR key. A deposit is a call you sign; everything private goes through the relayer.
  • Proofs are made where the SDK runs. The prover needs the circuits' artifacts, checked against their sha256 in manifest.json.
  • The relayer checks every proof before sending it, and can refuse. It cannot change what was proved.