@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");
Pay as a link
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
Export
What it is for
TESTNET, MAINNET
Where 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, accountUrl
NearBlocks links for a transaction or an account
Identity
Export
What 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_MESSAGE
The seed behind a NEP-413 wallet signature of the fixed message
encodeAddress, decodeAddress, isAddress
hushd1… addresses: bech32m of the receive key and the X25519 key
Private wallet
Export
What 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
Claim links
Export
What 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, newLink
The 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
Board posts, and the ones that open for an identity
loadTree(net)
The pool's Merkle tree rebuilt from its stored leaves
view, accountBalance, txStatus, latestBlock
Plain 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.
Cookies
No ad or tracking cookies. Essential only keeps what the site needs. Accept all adds anonymous, cookieless page view counts. More
We set no ad or tracking cookies. Essential only keeps what the site needs to work. Accept all turns on anonymous page view counts, with no cookies. See Cookies and Privacy.