Skip to main content
A private swap trades one token for another entirely from your shielded balance. The sell token is withdrawn from the privacy pool to avnu’s executor, the swap runs through avnu’s solver-optimized routing, and the bought token lands back in your private balance as a new note.
Preview. Requires @avnu/avnu-sdk >= 4.1.0-next.2 and the Starknet privacy SDK. Questions? Reach out on Telegram.
Prerequisite: the sell token must already be in your private balance. Deposit into the privacy pool first.

Setup

The snippets below assume a few objects are already wired up. Here is where each one comes from:
IdentifierSource
getQuotes, quoteToCalls, serializeCalls@avnu/avnu-sdk
accountyour Starknet account (e.g. starknet.js Account)
transfers, provingBlockId, Openthe Starknet privacy SDK
paymasterthe privacy SDK’s paymaster client, pointed at avnu’s paymaster endpoint
POOLthe privacy pool address (from the SDK or the team)
paymaster is the client that calls avnu’s paymaster service at starknet.paymaster.avnu.fi (or sepolia.paymaster.avnu.fi for testing). It is not starknet.js’s PaymasterRpc: that client only handles the standard invoke and deploy types, while private transactions use apply_action and invoke_and_apply_action, which the privacy SDK’s client builds and submits. For the sponsored and sponsored_private modes, give it your Portal API key, the same one used for gasfree.

How it works

A private swap is an apply_action transaction. No user signature is needed, since every call settles on-chain straight from the proof:
  1. Quote and private calls. Call getQuotes, then quoteToCalls({ private: true }). The backend sets takerAddress = executor and returns the inner swap calls plus the executorAddress.
  2. Build. Call paymaster.buildTransaction with fee_mode: "sponsored_private". The server returns the fee_action, the pool fee you need to withdraw in your chosen token.
  3. Proof. Using the privacy SDK, withdraw the sell token to the executor, withdraw the pool fee, and open a note for the bought token, then invoke the executor with the serialized swap calls.
  4. Execute. paymaster.executeTransaction relays the proof. The relayer pays gas and the forwarder collects the pool fee.

Step by step

1. Quote and build private swap calls

Pass private: true to quoteToCalls. The backend routes the swap through avnu’s executor and returns its address, so you do not set takerAddress on quoteToCalls yourself.
import { getQuotes, quoteToCalls } from '@avnu/avnu-sdk';

const STRK = '0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d';
const ETH  = '0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7';

// Fetch a solver-optimized quote
const [quote] = await getQuotes({
  sellTokenAddress: STRK,
  buyTokenAddress: ETH,
  sellAmount,
  takerAddress: account.address,
  size: 1,
});

// Build private swap calls (the backend sets takerAddress = executor)
const { calls, executorAddress } = await quoteToCalls({
  quoteId: quote.quoteId,
  slippage: 0.05, // 5%
  private: true,
});

2. Build the sponsored-private transaction

Call buildTransaction with the apply_action type and the sponsored_private fee mode. The pool_fee_token is the token you want to pay the pool fee in (here, STRK). The response’s fee_action tells you the recipient and amount to withdraw for the pool fee.
const build = await paymaster.buildTransaction({
  transaction: {
    type: 'apply_action',
    apply_action: { pool_address: POOL },
  },
  parameters: {
    version: '0x1',
    fee_mode: { mode: 'sponsored_private', pool_fee_token: STRK, tip: 'normal' },
  },
});

3. Build the proof

With the privacy SDK’s transfers builder, on the sell token withdraw the sellAmount to the executor, withdraw the pool fee (build.fee_action), and route any surplus back to the user. On the buy token, open a note to receive the output, then invoke the executor with the serialized swap calls and the opened note id.
const { call, proof } = await transfers
  .build()
  .with(STRK, (t) => {
    t.withdraw({ recipient: executorAddress, amount: sellAmount });
    t.withdraw({ recipient: build.fee_action.recipient, amount: build.fee_action.amount });
    t.surplusTo(account.address);
  })
  .with(ETH, (t) => t.transfer({ recipient: account.address, amount: Open }))
  .invoke(({ openNotes }) => ({
    contractAddress: executorAddress,
    calldata: [ETH, ...serializeCalls(calls), openNotes[0].noteId],
  }))
  .execute({ provingBlockId });
Open is a privacy SDK sentinel that opens a note for the swap output, whose amount is only known after execution. See Setup for where transfers, paymaster, and the other objects come from.

4. Execute

Relay the proof through the paymaster. No signature is needed for apply_action, since everything settles from the proof on-chain.
await paymaster.executeTransaction({
  transaction: {
    type: 'apply_action',
    apply_action: {
      apply_actions_call: call,
      proof: proof.data,
      proof_facts: proof.proofFacts,
    },
  },
  parameters: {
    version: '0x1',
    fee_mode: { mode: 'sponsored_private', pool_fee_token: STRK, tip: 'normal' },
  },
});

Full example

Sponsored Private Swap (STRK → ETH)
import { getQuotes, quoteToCalls } from '@avnu/avnu-sdk';

const STRK = '0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d';
const ETH  = '0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7';

// 1. Get quote + build swap calls with private: true
//    Backend automatically sets takerAddress = executor, returns inner calls + executorAddress
const [quote] = await getQuotes({
  sellTokenAddress: STRK,
  buyTokenAddress: ETH,
  sellAmount,
  takerAddress: account.address,
  size: 1,
});
const { calls, executorAddress } = await quoteToCalls({
  quoteId: quote.quoteId,
  slippage: 0.05,
  private: true,
});

// 2. Build (server returns fee info)
const build = await paymaster.buildTransaction({
  transaction: {
    type: 'apply_action',
    apply_action: { pool_address: POOL },
  },
  parameters: {
    version: '0x1',
    fee_mode: { mode: 'sponsored_private', pool_fee_token: STRK, tip: 'normal' },
  },
});

// 3. Generate proof: withdraw sell token to executor + fee + open note for buy token
const { call, proof } = await transfers
  .build()
  .with(STRK, (t) => {
    t.withdraw({ recipient: executorAddress, amount: sellAmount });
    t.withdraw({ recipient: build.fee_action.recipient, amount: build.fee_action.amount });
    t.surplusTo(account.address);
  })
  .with(ETH, (t) => t.transfer({ recipient: account.address, amount: Open }))
  .invoke(({ openNotes }) => ({
    contractAddress: executorAddress,
    calldata: [ETH, ...serializeCalls(calls), openNotes[0].noteId],
  }))
  .execute({ provingBlockId });

// 4. Execute
await paymaster.executeTransaction({
  transaction: {
    type: 'apply_action',
    apply_action: {
      apply_actions_call: call,
      proof: proof.data,
      proof_facts: proof.proofFacts,
    },
  },
  parameters: {
    version: '0x1',
    fee_mode: { mode: 'sponsored_private', pool_fee_token: STRK, tip: 'normal' },
  },
});

Key parameters

private
boolean
On quoteToCalls, builds swap calls for private execution. The backend sets takerAddress = executor and returns executorAddress alongside the inner calls. Do not pass takerAddress to quoteToCalls when private: true.
fee_mode.mode
string
Use sponsored_private so the relayer pays gas and the user pays the pool fee from their private balance. Only valid for private transaction types, otherwise it returns error 168.
fee_mode.pool_fee_token
string
Token used to pay the pool fee (e.g. STRK, ETH, USDC). The paymaster converts the base STRK amount to this token via its price oracle and returns the result in build.fee_action.amount.
Other fee modes. Replace sponsored_private with sponsored (and drop pool_fee_token) to always pay the pool fee in STRK, or gasless to have the user pay both gas and pool fee from their private balance. See the fee modes table.

Privacy Overview

Privacy pool, fee modes, and transaction types

Get Quotes

Fetch solver-optimized swap quotes