Skip to main content
A Chipi passkey wallet can trust a second wallet the user already has. Bramble is the running example here; Braavos, Ready (Argent) and other Chipi wallets work the same way.
  • Co-owner: signs anything the passkey can, such as approving or revoking an agent session, or replacing a lost passkey.
  • Guardian: can only start a recovery. The recovery completes after 7 days unless an owner cancels it.
@chipi-stack/* 14.17.0+ ships the pieces: the STARK_SNIP12 signer kind, sdk.executeWithExternalSigner / useExternalSignerExecute, and the governance builders and readers used below.
The STARK_SNIP12 verifier class (0x017884125977631bee198b34cd3e3fcf975a543e453c8b996a818910a964b5e) is declared on Starknet mainnet (2026-10-14, tx 0x00ba3329…59e6, block 16377651). Step 2 still checks isShhhVerifierDeclared first, so the same code refuses to propose it on a network where it isn’t.
Both roles assume the wallet’s threshold is 1, the default. On a SHHH V8.5 wallet whose threshold you raise above 1, a co-owner of weight 1 can no longer sign alone (only a recovery cancel stays open to it); on V8.4 the threshold isn’t enforced for single signers, which is why Chipi keeps V8.4 thresholds at 1. See Co-signing.

Why a new signer kind

Bramble uses a STARK key, the same curve as the wallet’s own STARK signer. But a browser wallet never signs a raw hash. Its only signing method, wallet_signTypedData, signs a SNIP-12 message hash that always includes the signing wallet’s own address. So Bramble can’t produce the signature the STARK verifier checks. STARK_SNIP12 wraps the Chipi wallet’s transaction hash (its SNIP-12 outside-execution hash) in a typed data Bramble signs as-is:
The verifier rebuilds it on-chain and checks the STARK signature. A signature only works for one Chipi wallet, on one chain, for one transaction. The verifier never calls the Bramble account, so a brand-new Bramble account that is not deployed yet works too. What each wallet returns from wallet_signTypedData. The SDK accepts all three and sends the verifier [r, s]: Not supported: multisig accounts, Ready accounts with a guardian, and Braavos hardware or passkey signers. A co-owner has full control of the wallet. If the user only wants a backup, make Bramble a guardian. You pick the role in step 2.

1. Read Bramble’s key (browser)

The verifier is bound to Bramble’s STARK key and account address. Read them from one signature: connect Bramble from a click, have it sign an Approval, then resolve the key.
resolveStarkSnip12Signer reads the key from the signature (Ready), the account (get_public_key for Bramble’s OpenZeppelin account, get_signers for Braavos), the deployment data (a new account), or the owner record (a Chipi wallet). It throws unless that key verifies the signature.

2. Propose (passkey, one prompt)

A Chipi wallet starts with only the STARK verifier. Propose the new kind and the new owner in the same transaction. Both have a 48-hour timelock. role is "OWNER" for a co-owner or "GUARDIAN" for recovery only; role and label must be the same in step 3, because the timelock commits to them.
Send proposeCalls with the passkey, like any other call on the wallet (useCallAnyContract with usePasskey: true, contractAddress set to the wallet). Wait for the transaction, then read the op ids from its events: they include the block time, so they can’t be computed in advance.

3. Execute (after 48 hours)

Execute calls are permissionless, so anyone may send them once the timelock is over: the passkey again, or Bramble itself with wallet_addInvokeTransaction (Bramble pays its own gas in STRK). Register the verifier first: execute_add_owner checks the new key through it.

4. Sign with Bramble (co-owner)

Only when Bramble was added with role: "OWNER". Bramble signs an Approval for the transaction; Chipi’s paymaster relays it gaslessly. Here Bramble approves a capped agent session:
In React, useExternalSignerExecute() from @chipi-stack/nextjs (or @chipi-stack/chipi-react) takes the same input. Wait for the transaction before treating it as done. Bramble shows the typed data with the wallet address and a hash, not the decoded calls. Show the calls in your own UI before asking for the signature.

5. Recover

Both recovery paths below work on-chain today. What Chipi does not support yet is pointing the wallet record at a new passkey: the record keeps the old encrypted key and passkey id, and the SDK has no call to replace both. So after a recovery, the recovered key signs with executeShhhWithStarkKey, not with the passkey hooks. Keep that key in memory, or encrypt it yourself, until record updates ship. The user can still sign in to your app with Bramble (Sign in with Starknet), which gives you the bearer token these calls need. With a co-owner. Nothing is locked: Bramble keeps signing everything. To replace the lost passkey key, Bramble signs buildProposeRotateOwnerCall({ walletAddress, proposer: brambleId, ownerId: 0, newPubkeyBytes: [newKey] }) through executeWithExternalSigner, with a fresh STARK key generated in the browser. After 24 hours anyone executes the rotation (buildExecuteRotateOwnerCall with the op id from the propose events). newKey then signs as owner 0: executeShhhWithStarkKey({ privateKey, ownerIndex: 0, ... }). With a guardian (7 days).
  1. Generate a fresh STARK key K in the browser. Bramble signs buildInitiateRecoveryCall({ walletAddress, proposer: brambleId, newOwner: { kind: "STARK", pubkeyBytes: [K], role: "OWNER", weight: 1, label: "recovered" } }) through executeWithExternalSigner. A guardian’s signature is accepted for this one call and nothing else.
  2. During the 7 days, any owner can cancel with buildCancelRecoveryCall, passing the cancelling owner’s id. Guardians can’t cancel. Watch readPendingRecovery and alert the user, so a recovery they didn’t start gets cancelled.
  3. After 7 days anyone sends buildFinalizeRecoveryCall with the same new-owner fields, for example Bramble with wallet_addInvokeTransaction (it pays its own gas in STRK). Recovery adds K as a new owner; it never removes or replaces one.
  4. K signs with executeShhhWithStarkKey({ privateKey: K, ownerIndex: <K's id from readShhhOwners>, ... }). Remove the lost owner with buildProposeRemoveOwnerCall (24 hours) if it may be compromised.
Before starting a recovery, check that it can finalize: the new key’s kind is registered on the wallet (STARK always is), the key isn’t already an owner, and the weight is above 0. Only an owner can clear a recovery that can’t finalize, and the user who needs it has none.

Bramble rules to follow

From Bramble’s integration guide (v0.10.5):
  • window.starknet_bramble can appear after page load. Wait for the starknet#initialized event before deciding it is not installed.
  • Prompts open only from a click. Don’t auto-retry: Bramble allows 5 pending prompts per site.
  • Every params object is checked strictly. Never send undefined values or extra keys.
  • The typed data’s chainId must match Bramble’s active network, or it answers 114 before any prompt. Check wallet_requestChainId is 0x534e5f4d41494e (mainnet) first.
  • 113 means the user declined. 163 covers a locked wallet, a site that isn’t connected, and other refusals: ask the user to unlock Bramble and retry.
✅ Verified against the SDK and contract source on 2026-10-14.