- Add a new device to a wallet they can still sign with (48-hour timelock). The canonical “switching from old phone to new” case.
- Recover a wallet they can no longer sign with at all (7-day timelock). A pre-registered guardian adds a fresh device as a new owner.
- Change the PIN that encrypts their wallet’s signing key (instant). For users who remember the old PIN and just want to pick a new one — or upgrade to a passkey.
SHHH-side flows are available without advertisement until external audit closes. Both are on-chain since 2026-05-26 with shipped mainnet smokes — see scripts/receipts/.
Flow 1 — Add device (user still has wallet access)
Use when the user wants a second device controlling the same wallet (the canonical “switching from laptop to phone” case) or wants to add a guardian after onboarding. Stages:- Propose — an existing owner signs an outside execution containing
propose_add_owner. The wallet records a pending op taggedADD_OWNERand starts a 48-hour timelock. Theop_idmixes in the block timestamp and a counter, so read it from theOpProposedevent in the receipt (parseOpProposedEvents). - Wait —
useGuardianRecovery().isReady(validAfter)andsecondsRemaining(validAfter)drive a countdown UI. They don’t read the chain: keep theopIdandvalidAfterfrom step 1. - Execute — after 48h, anyone can submit
execute_add_ownerwith the matchingop_id: the execute calls are permissionless once the timelock has passed. The wallet re-derives the proposed payload and asserts equality before it changes the owner set.
newOwner must be the same in the propose and the execute: the wallet compares the payload hash.
Flow 2 — Guardian recovery (user lost their key)
Use when the user can no longer sign with any existing owner. A pre-registered guardian (a wallet the user already has, a second device, or a “Guardian-as-a-Service” provider) starts the recovery on the user’s behalf. Stages:- Initiate — the guardian (not the lost owner) signs an outside execution whose only call is
initiate_recovery, withproposerset to the guardian’s own owner id. Anyone can relay that signed OE. The wallet stores the new owner’s commitment and starts a 7-day timelock. Only one recovery can be pending at a time. - Wait — 7 days. During this window any current owner that still has its key (role
OWNER, not revoked) cancancel_recovery. This is the safety valve against a malicious guardian. If no owner key survives, nobody can cancel: the 7 days are only a delay. - Finalize — after 7 days, anyone can submit
finalize_recoverywith the same new-owner arguments. The wallet adds that owner. It removes nobody and leaves the threshold as it is.
op_id. Read it with readPendingRecovery.
propose_remove_owner (24h) from the recovered device.
The new owner must be of a kind the wallet already verifies. finalize_recovery checks the new key through that kind’s verifier class, and a user who lost every key can no longer add a verifier class. Recover onto the wallet’s own kind, or onto a kind registered while an owner could still sign.
Bramble as the guardian
A wallet the user already has can be the guardian: Bramble, Braavos, Ready or another Chipi wallet, registered with theSTARK_SNIP12 kind. Its key never leaves that wallet. It signs the recovery from a click, and Chipi’s paymaster relays it:
The
STARK_SNIP12 verifier class is declared on mainnet (2026-10-14). See Trusted wallets for the full Bramble setup.New signer kinds need a verifier class
A SHHH wallet verifies only the kinds in its own verifier-class map. Its constructor registers one: the kind of its first owner (STARK for a Chipi passkey wallet). To add an owner or guardian of another kind (STARK_SNIP12 for Bramble, WEBAUTHN_P256 for a device passkey, EIP191_SECP256K1 for MetaMask, and so on), the wallet needs that kind’s verifier class first:
- Check with
readVerifierClass(provider, walletAddress, kind).0nmeans the kind is not registered. For a kind new on the network, also checkisShhhVerifierDeclared. - An owner signs one OE with
buildProposeAddVerifierClassCallandbuildProposeAddOwnerCall(both 48h). - After 48h, submit
buildExecuteAddVerifierClassCallbeforebuildExecuteAddOwnerCall.execute_add_ownerchecks the new key through the verifier and reverts with'SHHH: verifier missing'otherwise.
OpProposed event; parseOpProposedEvents(tx.events, walletAddress) returns both, with their opKind (ADD_VERIFIER, ADD_OWNER).
Cancel safety valve
Within either timelock window, an owner can cancel. The two flows use different entrypoints:- Add-device uses
cancel_pending_op(op_id). It cancels any pending governance op (ADD_OWNER,REMOVE_OWNER,ROTATE_OWNER,SET_THRESHOLD,ADD_VERIFIER,REMOVE_VERIFIER), signed by an owner. - Guardian recovery uses
cancel_recovery(owner_id).owner_idis the id of the owner who cancels, not of an owner being replaced: the wallet checks that it is a non-revoked owner with roleOWNER. Pass the signing owner’s own id. There is only one pending recovery, so no op id is needed.
<Recover /> shows a Cancel button when you pass an onCancel callback; pick the matching builder for the mode:
Timelock constants
The expiry is measured from
valid_after: expires_at = proposed_at + timelock + 14d. A 48h op can be executed from day 2 to day 16 after the propose. parseOpProposedEvents returns the exact validAfter and expiresAt.
These constants are re-exported on the hook return for UI display: useGuardianRecovery().TIMELOCK_SECONDS and .DEFAULT_OP_EXPIRY_SECONDS. TIMELOCK_SECONDS also lists ADD_GUARDIAN and REMOVE_GUARDIAN (24h); the wallet has no separate guardian ops, so a guardian is added with propose_add_owner (48h) and removed with propose_remove_owner (24h).
Who acts as guardian?
Patterns integrators ship:- A wallet the user already has — Bramble, Braavos, Ready or another Chipi wallet, as a
STARK_SNIP12guardian. See Trusted wallets. - Second wallet held by the user — a second device or a paper-backup-rooted wallet. Simplest; no third party.
- Email-rooted WebAuthn credential — register a passkey against the user’s email-bound device at onboarding; that credential is the guardian. The user doesn’t need a second wallet day-one.
- Guardian-as-a-Service — a paid third party (could be Chipi, could be a partner) holds a guardian key, with a published policy on when they sign recoveries (e.g., on a 48h email + 2FA confirmation). This is on our roadmap; the productization design is internal-only for now.
role: "GUARDIAN" is set when the guardian is added. A guardian can sign only an OE whose single call is initiate_recovery with proposer equal to its own owner id. It can’t sign transactions, move funds, or cancel a recovery.
What does NOT recover
Recovery adds one owner to the wallet’s owner set. It does NOT:- Remove the lost owners (follow up with
propose_remove_owner, 24h) - Change the threshold
- Reset session keys (those remain valid until their own
valid_until) - Re-create CHIPI v29 STARK key encryption (those wallets don’t have on-chain recovery at all — see migration to move to SHHH first)
- Touch USDC balances or any other on-chain state — recovery adds a key, it is not a state rollback
Change a PIN (any wallet type)
The two on-chain flows above handle “user can’t sign at all.” A simpler flow handles “user remembers their PIN but wants to change it” — for example, they want to upgrade to a passkey, or they suspect their PIN was shoulder-surfed. This is client-side re-encryption of the same private key, then one backend call to store the new ciphertext. The wallet’s address never changes.useMigrateWalletToPasskey hook which wraps this same flow with the WebAuthn ceremony.
Related
- Add a second device — proactively register a backup device before the user needs to recover
- Upgrade existing wallets — get a CHIPI v29 wallet onto SHHH first to use the on-chain recovery flows
- When passkeys fail — the matching browser-side playbook for passkey failure modes
