Privy lets your users sign in the way they already do - email, social login, passkey, or an existing wallet - and gives each one a self-custodial embedded wallet with no seed phrase to manage. Because the Stabletrust SDK only needs a standard ethers signer to operate, a Privy wallet plugs straight in: the same wallet the user signs in with holds their confidential balance and runs deposits, transfers, and withdrawals. Privy’s role is unchanged. It authenticates the user, manages the embedded wallet, and produces signatures. Stabletrust uses those signatures to derive the user’s encryption key and to broadcast confidential transactions - it never sees or stores the wallet’s private key.
This is the recommended path when you don’t want to custody raw private keys. The Stabletrust API ConfidentialTransferClient accepts any ethers.Signer, so you pass a Privy signer instead of building an ethers.Wallet from a raw key. For a complete Next.js app built on this pattern, see Building an App with the SDK.

Compatibility

How the integration works

Three pieces cooperate, and the wallet’s private key stays inside Privy the whole time:
  • The user’s Privy wallet - produces every signature. That includes the one-time key-derivation signature and each on-chain transaction. Nothing moves without it.
  • The @fairblock/stabletrust SDK - runs in your app. It derives the user’s encryption key from a wallet signature, generates the zero-knowledge proof locally in WebAssembly, and builds the confidential transactions.
  • The chain - the Privy wallet broadcasts the transaction directly. Amounts are encrypted on-chain; the sender and recipient addresses remain visible. There is no Fairblock enclave or relayer in this flow.
A confidential transfer hides the amount, not the participants. The sender and recipient addresses are visible on-chain - only the value moved is encrypted. To also hide the sender’s address, use Unlinkable Transfers.

Installation

You will also need a Privy App ID from the Privy dashboard and a JSON-RPC endpoint for your target network.

1. Get a signer from Privy

Stabletrust expects a standard ethers.Signer. Privy exposes its wallet as an EIP-1193 provider, so wrap that provider with ethers and take the signer from it.
This is the exact signer used throughout Building an App with the SDK. Everything below is identical no matter how the signer was produced.
On the server, use @privy-io/server-auth to obtain a wallet for an agent, wrap its EIP-1193 provider with ethers.BrowserProvider the same way, and use the resulting signer with the SDK unchanged.

2. Point the SDK at the signer

Create one ConfidentialTransferClient for the network, then call ensureAccount with the Privy signer. ensureAccount triggers a single wallet signature, derives the user’s encryption keypair from it, and - the first time only - registers the public key on-chain.

3. Read the confidential balance

The balance is stored encrypted on-chain. The SDK decrypts it client-side with the privateKey from ensureAccount - the wallet is not needed for a read.

4. Deposit (shield)

Move public ERC-20 tokens into the encrypted balance. The SDK handles the ERC-20 approval for you - no separate approve call.

5. Transfer confidentially

Send tokens from the sender’s encrypted balance to another address’s encrypted balance. The amount is hidden on-chain; the recipient must already have called ensureAccount.

6. Withdraw (unshield)

Move tokens from the encrypted balance back to a public ERC-20 balance. After withdrawal the amount is visible on-chain again.

Putting it together

Notes and security

  • Self-custodial. The wallet’s private key stays inside Privy’s infrastructure. Stabletrust only ever receives signatures and a derived encryption key - it cannot move the user’s funds.
  • The derived key never leaves the client. keys.privateKey is a decryption key derived in the browser from a signature. It is re-derived on each session and is not the wallet’s private key. Keep it in memory only; never log or persist it in plaintext.
  • Keys are per chain and contract. The same wallet derives a different confidential key on each network. Call ensureAccount once per chain you support.
  • Account finalization takes time. The first ensureAccount waits for on-chain finalization (roughly 45 seconds). It resolves once the account is ready.
  • Recipients must exist. confidentialTransfer fails if the recipient has never called ensureAccount. Verify the recipient account exists before sending.

Next steps

Build an App with the SDK

The full Next.js walkthrough this integration is based on.

Turnkey

The same flow with a Turnkey embedded wallet.

Method Reference

Every client method, parameter, and return type.

Unlinkable Transfers

Hide the sender’s address as well as the amount.