Shadow Accounts

Shadow accounts give one user a persistent, pseudonymous identity for one dapp. They are useful for positions that live across transactions - staking, lending, rewards, and any protocol that needs to recognize a returning caller.

This page follows Privacy SDK 0.14.3-rc.8. The old subaccounts(...) API was renamed and is not compatible with the current anonymizer. Use shadowAccounts(...) throughout.

Snippets assume transfers, account and provider from Getting Started.

Configure the anonymizer

Pass the deployment for the connected network when creating transfers:

const transfers = createPrivateTransfers({
  account,
  viewingKeyProvider,
  provingProvider,
  discoveryProvider,
  poolContractAddress: process.env.POOL_ADDRESS!,
  shadowAccountAnonymizerAddress: process.env.SHADOW_ACCOUNT_ANONYMIZER_ADDRESS!,
})

Calling shadowAccounts(...) without shadowAccountAnonymizerAddress throws. Use the current Mainnet or Sepolia value from Deployed Contract Addresses.

Commitments and addresses

The builder derives the partial and full commitments locally from the user, viewing key, anonymizer, dapp name, and nonce:

import { shadowAccountAddress } from "@starkware-libs/starknet-privacy-sdk"

const shadow = transfers.build().shadowAccounts("myDapp")
const partial = await shadow.partialCommitment()
const commitment = await shadow.commitment(0n)

const address = shadowAccountAddress(
  commitment,
  BigInt(process.env.SHADOW_ACCOUNT_ANONYMIZER_ADDRESS!),
)

shadowAccountAddress uses the exported, fixed PRIMER_CLASS_HASH, matching the current Cairo deployment pattern. It works before the account is deployed and does not require an RPC call. The package also exports the lower-level shadowAccountPartialCommitment and shadowAccountCommitment helpers when you already hold the raw felts.

Invoke through the shadow account

Fund the shadow address, create an open note for each expected output token, then queue the calls. The SDK translates this into one ComputeAndInvoke against the ShadowAccountAnonymizer.

import { Open } from "@starkware-libs/starknet-privacy-sdk"

const provingBlockId = (await provider.getBlockNumber()) - 10

const { callAndProof } = await transfers
  .build({ autoDiscover: { notes: "refresh" } })
  .with(STRK)
  .withdraw({ recipient: address, amount: 5n * 10n ** 18n })
  .surplusTo(account.address, false)
  .with(STRK)
  .transfer({ recipient: account.address, amount: Open })
  .done()
  .shadowAccounts("myDapp")
  .invoke(0n, {
    calls: [stakingContract.populate("stake", { amount: 1000n })],
    collectPolicy: { type: "all" },
  })
  .execute({ provingBlockId })

// Submit callAndProof with the usual proofDetails + tip: 0n tail.

collectPolicy is optional in the SDK and defaults to { type: "all" }.

collectPolicy Amount collected into each open note
{ type: "all" } The token's entire shadow-account balance
{ type: "diff" } Only the balance gained during this interaction
{ type: "exact", amount } Exactly amount

Read deployed and undeployed accounts

The exported ShadowAccountAnonymizerABI includes these views:

  • get_shadow_accounts(partial, start, end, until_undeployed) resolves a nonce range and returns { nonce, address, is_deployed } for each entry.
  • get_shadow_account(commitment) returns the stored address of an already deployed account, or zero when it has not been deployed.
  • get_shadow_account_class_hash() returns the class installed after the primer deployment. It is not the class hash used for address derivation.

The scan range is limited to 1,024 nonces. With until_undeployed: true, the range stops at the first address that has not been deployed.

Things to notice

  • A shadow account's balances, calls, and positions are public. Privacy comes from hiding the link to the user's main account.
  • A shadow account has no keys. Only the anonymizer can execute through it.
  • One transaction can contain at most one invoke-phase action, whether it is a normal invoke or a shadow-account invocation.
  • One collection policy applies to every open note settled by the invocation.
  • The current anonymizer returns the shadow-account address used by deposits so the pool can apply its screening policy to that address.

Next: Proving Configuration - prepare and submit the resulting call safely.