The Stabletrust SDK ships two clients that serve different privacy models. This guide covers the AnonymousTransferClient, which hides the sender’s wallet address entirely. If you only need encrypted amounts and balances (with a visible sender), use the ConfidentialTransferClient covered in Building an App with the SDK.

Confidential vs. Unlinkable: which client?

With AnonymousTransferClient, transactions are routed through the Fairycloak relay, which submits them onchain and pays gas on your behalf (except deposits). The sender’s wallet address is never revealed onchain: only a numeric anonymous account ID is associated with the transfer.
Access required. Unlinkable transfers are available to teams building privacy-critical applications. To obtain a Fairycloak relay URL and API key, reach out to the Fairblock team at hello@fairblock.network.

Installation

Requires Node.js 18+ and ethers.js 6+ (installed automatically as a dependency).

Unlinkable account ID rules

Every accountId (and senderAccountId / recipientId) you pass must follow these rules, enforced both onchain and by the Fairycloak relay:
  • Non-empty string
  • At most 20 characters long
  • Alphanumeric characters only (A-Z, a-z, 0-9): no spaces, punctuation, or Unicode
  • Case-sensitive: "MyAccount" and "myaccount" are different accounts; no normalization is applied
The SDK validates these fields before making any request and throws a descriptive Error if they don’t follow the rules.

Initialization

Key derivation

Unlinkable accounts use a per-account ElGamal keypair derived from a wallet signature. Store the returned privateKey securely: it cannot be recovered without the original wallet and account ID.
An auth signer can only ever be bound to a single unlinkable account. Reusing a wallet across different accountIds will fail at creation time.

End-to-end unlinkable flow

Key methods

Account management

  • createAccount(authWallet, accountId, elgamalPublicKey, options?): Creates a new unlinkable account via the relay (relay pays gas). accountId is a caller-chosen string following the account ID rules.
  • ensureAnonymousAccount(authWallet, accountId, elgamalPublicKey, options?): Idempotent helper: creates the account if it doesn’t exist and (by default) waits until it has settled. Safe to call on every run with a stable accountId. Returns { accountId, accountInfo, created }. Options: deadlineOffset (default 3600), waitUntilReady (default true), timeoutMs (default 180000), pollIntervalMs (default 2000).
  • updateAuthKeys(authWallet, accountId, { add, remove }, options?): Adds or removes authorised signers.
  • getAnonymousAccountInfo(accountId): Returns onchain state: exists, finalized, hasPendingAction, txId, elgamalPubkey, authNonce.
  • isAuthorizedSigner(accountId, signerAddress): Checks whether an address is an authorised signer for the account.

Balances

  • getBalance(accountId, tokenAddress, elGamalPrivateKey): Returns decrypted balance totals: { amount, available, pending }.
  • getAnonymousBalance(accountId, tokenAddress, elGamalPrivateKey): Returns decrypted balances including raw ciphertexts (useful for generating proofs manually): { available: { amount, ciphertext }, pending: { amount, ciphertext } }.

Moving funds

  • deposit(authWallet, accountId, tokenAddress, amount, options?): Deposits tokens into an unlinkable account. The user pays gas. Handles ERC-20 approval automatically.
  • transferToPublic(authWallet, accountId, params, options?): Transfers from an unlinkable account to a public EVM address (relay pays gas).
  • transferToAnonymous(authWallet, senderAccountId, params, options?): Transfers between two unlinkable accounts (relay pays gas).
  • applyPending(authWallet, accountId, options?): Moves a pending incoming balance into available. Must be called after receiving an unlinkable-to-unlinkable transfer before the recipient can spend.
  • withdraw(authWallet, accountId, params, options?): Withdraws from an unlinkable account to a public EVM address (relay pays gas).

Manual proof mode

transferToPublic supports an auto-proof mode (recommended: pass elGamalPrivateKey and amount) and a manual proof mode for advanced use:

Prepaid fees

Every transferToPublic and transferToAnonymous call deducts a protocol fee from the sender account’s prepaid fee reserve. The SDK checks this balance before submitting and throws a descriptive error if it’s insufficient: top up with depositFees before transferring.
  • getFeeToken(): Returns the currently configured ERC-20 fee token address. depositFees must use this token. The fee token can be rotated by the protocol; existing balances in old fee tokens remain withdrawable.
  • getNonAnonymousTransferFee() / getAnonymousInlineTransferFee(): Return the per-transfer protocol fee (raw ERC-20 units). For unlinkable transfers use getAnonymousInlineTransferFee().
  • getPrepaidFeeBalance(accountId, token): Returns the prepaid fee balance for an account and fee token.
  • depositFees(authWallet, accountId, amount, options?): Deposits into the prepaid fee reserve. The user pays gas. Handles ERC-20 approval against the active fee token automatically.
  • withdrawFees(authWallet, accountId, { token, destination, amount }, options?): Withdraws a specific amount from a given fee token’s reserve (relay pays gas).
  • withdrawAllFees(authWallet, accountId, { destination }, options?): Withdraws all prepaid fee balances across all (including historical) fee tokens (relay pays gas).

Request tracking

All relay operations return a { request_id, tx_hash, status } object. Track completion with:

Security considerations

  • Private keys: Unlinkable account privateKey values are as sensitive as wallet private keys. Never log them; store them in encrypted vaults or hardware-backed key stores.
  • One signer per account: An auth signer binds to a single unlinkable account. Manage authorised signers with updateAuthKeys.
  • Apply pending before spending: Unlinkable-to-unlinkable recipients must call applyPending() before the received balance is spendable.
  • Network: Use HTTPS-only RPC endpoints and verify contract addresses before initialization.