Skip to main content
The engine behind Private Balances and the React hooks. Use this layer directly for headless/server flows; apps should prefer the hooks.

The companion account

The StarkWare pool requires its user_addr to implement SNIP-6 is_valid_signature, which advanced account classes (like Chipi’s SHHH wallets) deliberately do not expose. Instead of changing an audited account class, each user gets a companion: a lightweight standard account (any SNIP-6 class, e.g. OpenZeppelin 0x061dac032f228abef9c6626f995015233097ae253a7f72d68552db02f2971b8f) that acts as the pool identity.
  • Derived, not created: deriveCompanionKeypair(anchor) + deriveCompanionAddress({publicKey, classHash}) are deterministic from the user’s owner-independent anchor. Cache the address; never the keys.
  • Counterfactual: it exists on-chain only after first use. execShield deploys it automatically (self-paid from gas-tank-fronted STRK, salt = pubkey so the deployed address equals the derived one).
  • Never holds user funds at rest: deposits pass through it into the pool; withdrawals pass back out to the user’s real wallet; leftover fronted STRK sweeps back to the gas tank.
Why this design over changing the account class: see our write-up Adding confidential payments to an advanced smart account — two ways to use the StarkWare privacy pool, and why we picked the companion.

Headless example

import {
  ChipiProofProvider,
  derivePoolKeypair,
  deriveCompanionKeypair,
  deriveCompanionAddress,
  requestGasFunding,
  waitForFunding,
  execShield,
  GAS_FUND_STRK,
} from "@chipi-stack/core";
import { RpcProvider } from "starknet";

const pool = await derivePoolKeypair({ anchorSecret, seedDomainTag: wallet.address });
const companion = await deriveCompanionKeypair(anchorSecret);
const companionAddress = deriveCompanionAddress({
  publicKey: companion.publicKey,
  classHash: COMPANION_CLASS_HASH,
});

const provider = new RpcProvider({ nodeUrl: "https://api.cartridge.gg/x/starknet/mainnet" });
await requestGasFunding(companionAddress, {
  baseUrl: "https://api.chipipay.com/v1",
  apiKey: apiPublicKey,
  getBearerToken: () => getToken(),
});
await waitForFunding(provider, companionAddress, { strk: GAS_FUND_STRK });
const result = await execShield(
  {
    provider,
    chainId: "0x534e5f4d41494e",
    companionAddress,
    companionKey: companion.privateKey,
    classHash: COMPANION_CLASS_HASH,
    poolKey: BigInt(pool.privateKey),
    provingProvider: new ChipiProofProvider({
      baseUrl: "https://api.chipipay.com/v1",
      apiKey: apiPublicKey,
      getBearerToken: () => getToken(),
      chainId: "0x534e5f4d41494e",
    }),
  },
  USDC_ADDRESS,
  1_000_000n,
);
execShieldMany batches several tokens into ONE proof with ONE 4-STRK pool fee (mainnet-proven). execUnshield withdraws to the companion; deliver onward with sweepCompanion({ strict: true }).

Trust model

Keys derive client-side and are never stored server-side, but delegated proving includes the pool key in each proof request: Chipi’s private prover sees it per operation. Privacy holds against the public, not the proving operator. Details in the guide.