> ## Documentation Index
> Fetch the complete documentation index at: https://docs.chipipay.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Add a Second Device or Co-Signer

> Let one wallet be controlled by more than one signer — a passkey on the phone plus a passkey on the laptop, or a user plus an agent. Same on-chain account, multiple ways to approve.

A Chipi wallet starts with one owner: whatever passkey or signer the user registered at sign-up. This page covers the two ways to add more signers to that wallet:

1. **Additional devices that each act on their own.** Add the user's laptop passkey alongside their phone passkey. Either device can sign a transfer. This is what most consumer apps want.
2. **Co-signing where multiple signers must approve.** Require two signatures (or three of four, etc.) before any transfer goes through. This is what agent treasuries, shared wallets, and high-value accounts want.

Both routes share the same on-chain account. You're not creating a second wallet — you're widening who controls the existing one.

<Info>
  Available without advertisement until external audit closes. The on-chain
  logic shipped with SHHH V8.4 and has a mainnet smoke — see
  [scripts/receipts/](https://github.com/chipi-pay/sdks/tree/main/scripts/receipts).
</Info>

## When to use which

| Your use case                                                           | Path                                                                |
| ----------------------------------------------------------------------- | ------------------------------------------------------------------- |
| User wants their phone + their laptop to both work                      | Independent owners (each device signs on its own)                   |
| User wants a backup passkey on a second device "in case"                | Independent owners; same as above                                   |
| Agent spends from a treasury you co-control                             | Co-signing — typically 2-of-2 above a spend threshold, 1-of-2 below |
| Shared org wallet across finance + ops                                  | Co-signing — set the threshold to match your governance             |
| High-value personal account where any single device shouldn't be enough | Co-signing — 2-of-3 between phone + laptop + recovery contact       |

Concrete examples in production today:

1. **Agent treasuries** — an automated agent spends up to \$X/day on its own, anything bigger requires a co-sign from the user's passkey. 2-of-2 above the threshold, 1-of-2 below.
2. **AI-API service accounts** — a service account shares custody with a human operator. Routine API spend doesn't prompt; settlement transfers do.
3. **Co-signed org treasuries** — Chipi credits balance is N-of-M with key roles distributed across finance + ops + the founder.

## Compared to single-owner

Single-owner SHHH wallets sign via one V2\_SNIP12 envelope:

```
[V2_SNIP12_PREFIX, owner_id, kind_tag, ...payload]
```

A threshold wallet signs via a threshold envelope wrapping N inner envelopes:

```
[
  V2_THRESHOLD_PREFIX,
  threshold,        // N
  num_owner_sigs,   // count of inner envelopes that follow
  ...inner_envelope_1,
  ...inner_envelope_2,
  ...
]
```

Each inner envelope can be a **different signer kind**. The wallet validates each inner envelope against its registered verifier class, counts the verified ones, and admits the call only when `verified_count ≥ threshold`. The set of owners and the threshold N live on-chain.

## Owner kinds within a threshold

Anything that works as a single-owner signer kind works as a threshold owner. You can mix:

* An EOA on MetaMask (`EIP191_SECP256K1`) co-signs with a passkey (`WEBAUTHN_P256`)
* A Phantom / Solana wallet (`ED25519`) co-signs with a STARK key held server-side
* A JWT\_ES256 service account co-signs with a STARK key held server-side
* A guardian (`role: "GUARDIAN"`) does NOT count toward the threshold for normal transactions — guardians only initiate recovery, never co-sign

## Configuring N and M

The wallet is created with N initial owners (M=N at start). Add or remove owners post-creation via the recovery flows in [recovery](/services/gasless/recovery):

* `propose_add_owner` → `execute_add_owner` after 48h
* `propose_remove_owner` → `execute_remove_owner` after 24h
* `propose_set_threshold` → `execute_set_threshold` after 48h

Threshold changes are timelocked because they're governance-grade — a malicious add-owner that immediately set threshold=1 would be a wallet takeover. The 48h window gives the other owners a chance to `cancel_pending_op`.

## SDK status

| Surface                                                                                                     | Status                                                                                                   | Notes                                                              |
| ----------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| Backend builders (`buildThresholdEnvelope`, `buildProposeSetThresholdCall`, `buildExecuteSetThresholdCall`) | Shipped                                                                                                  | See `@chipi-stack/backend` exports                                 |
| Python builders                                                                                             | Shipped                                                                                                  | Mirrors TS — see `chipi_sdk.shhh.threshold`                        |
| React hook                                                                                                  | `useGuardianRecovery().buildProposeSetThreshold` / `.buildExecuteSetThreshold` cover the governance side | The N-of-M signature assembly hook (`useThresholdSign`) ships next |
| Mainnet smoke                                                                                               | Shipped (view-shape + parser)                                                                            | Real-money smoke deferred until external audit closes              |

```ts theme={null}
import {
  buildThresholdEnvelope,
  buildStarkEnvelope,
  buildEip191EnvelopeFromSignature,
} from "@chipi-stack/backend";

// Two owners co-sign the same OE.
const innerStark = buildStarkEnvelope({ privateKey: serverStarkKey, messageHash: oeHash });
const innerEip191 = buildEip191EnvelopeFromSignature({
  signatureHex: metaMaskPersonalSig,
  pubkey: { ethAddress: userMetaMaskAddress },
});

const envelope = buildThresholdEnvelope({
  threshold: 2,
  innerEnvelopes: [innerStark, innerEip191],
});
// Feed `envelope` into the same execute_from_outside_v2 calldata path
// you'd use for a single-owner OE.
```

### External wallets that don't expose a key (MetaMask, Phantom)

MetaMask and Phantom never hand you a private key — the user signs in their
wallet and you get a signature back. Each has a `*FromSignature` builder so you
can wrap that signature into an inner envelope:

| Wallet                      | Kind               | Builder                             | Sync?                        |
| --------------------------- | ------------------ | ----------------------------------- | ---------------------------- |
| MetaMask / any EVM EOA      | `EIP191_SECP256K1` | `buildEip191EnvelopeFromSignature`  | sync                         |
| Phantom / any Solana wallet | `ED25519`          | `buildEd25519EnvelopeFromSignature` | **async** (Garaga WASM init) |

For Phantom, the wallet must sign the **exact bytes** returned by
`ed25519SignedBytes(messageHash)` — the 64-byte hex-ASCII encoding of the OE
hash — not the raw 32-byte hash. The on-chain verifier reconstructs those same
bytes, so signing anything else fails verification.

```ts theme={null}
import {
  buildEd25519EnvelopeFromSignature,
  ed25519SignedBytes,
} from "@chipi-stack/backend";

// 1. Connect Phantom and read its 32-byte Ed25519 pubkey.
await window.solana.connect();
const publicKey = window.solana.publicKey.toBytes();

// 2. Sign the EXACT bytes the verifier expects (not the raw hash).
const msg = ed25519SignedBytes(oeHash);
const { signature } = await window.solana.signMessage(msg); // 64-byte R‖s

// 3. Wrap into an inner envelope (async — initializes Garaga on first call).
const innerEd25519 = await buildEd25519EnvelopeFromSignature({
  signature,
  publicKey,
  messageHash: oeHash,
});
// `innerEd25519` slots into `buildThresholdEnvelope({ innerEnvelopes: [...] })`
// exactly like the EIP-191 envelope above.
```

## Gas

The paymaster sums the per-kind gas overhead across each inner envelope. A 2-of-2 STARK + EIP-191 OE budgets 2M + 10M = 12M l2\_gas for verification on top of the call's own cost. See the [passkey API reference](/sdk/passkeys/api) for the kinds available client-side; gas tables live with the backend builders.

This composes — you don't budget gas yourself. The paymaster reserves it automatically based on the envelope shape.

## Threshold + recovery together

The headline product story for SHHH V8.4 is threshold combined with guardian recovery:

* **Day-to-day operations** require N-of-M signatures
* **Lost-key recovery** still works because a guardian can initiate `initiate_recovery` even if N-1 owners have lost their keys

A 2-of-3 wallet with one guardian = three signers required day-to-day; one guardian can recover the wallet to fresh owners if two are lost. This is the closest thing to "bank-grade custody without giving up custody" on Starknet today.

## Related

* [Passkey reference](/sdk/passkeys/api) — what each owner can be on the browser side
* [Recovery](/services/gasless/recovery) — add/remove owners + guardian recovery
* [Migration](/services/gasless/migration) — getting from CHIPI v29 to SHHH first
