Cavos

Multi-device

Add a new device to an existing wallet via approval from a registered device.

A wallet's address depends only on identity, so the same user on a new device lands on the same wallet. But that new device has its own key, which isn't yet an authorized signer on the account. Adding it requires approval from a device that already controls the wallet — the contract is the sole authority.

Detecting a new device

When Cavos.connect runs on a device whose key isn't an authorized signer, it returns status: "needs-device-approval" and records a pending request:

TypeScript
const cavos = await Cavos.connect({ /* ... */ });

if (cavos.status === "needs-device-approval") {
  // This device requested addition. Prompt the user to approve from an
  // existing device. Poll for approval, then reconnect.
  console.log(cavos.pendingRequestId);
}

Under the hood, when an appId is set, the kit uses the hosted HttpRecoveryClient to file a device-addition request, which notifies the user (styled as a login-approval prompt).

Approve with a passkey instead

If the user enrolled a passkey after signup, they can self-approve from the new browser with one prompt — no email round-trip, no second device:

TypeScript
import { Cavos, PasskeySigner } from "@cavos/kit";

const cavos = await Cavos.connect({ /* same identity + appSalt... */ });

if (cavos.status === "needs-device-approval") {
  await cavos.approveThisDeviceWithPasskey({ passkey: new PasskeySigner() });
  // cavos is now ready.
}

approveThisDeviceWithPasskey is the recommended path for users who have a passkey; the hosted approval page below is the fallback for those who don't.

Starknet mainnet caveat. Passkey approval currently works on Sepolia only. On Starknet mainnet, fall back to the hosted approval page (or recovery). Solana and Stellar route passkey approval through their relayers and are not affected.

Host your approval page (required)

Cavos does not host an approval page. Each integrating app must build its own /approve-device route and configure its origin in the dashboard. Approving a device signs an on-chain add_signer transaction paid for with the app's own paymaster API key — Cavos never sponsors gas for third-party apps.

When a device-addition request is filed, the backend builds the approval link as ${device_approval_url}/approve-device?request=<id> and emails it to the owner. Set device_approval_url in your app's dashboard (or website_url as a fallback) — if neither is configured, device addition is rejected.

The approving page you build does three things, on a device that is already an authorized signer:

  1. Fetch the pending request to read the new device's public key.
  2. Approve it by calling cavos.addSigner(newSigner) — this submits the on-chain add_signer call, gasless via your paymaster key.
  3. Confirm it with the recovery client so the backend marks the request approved.

Your page must construct its CavosConfig with the same appSalt, appId, and network the wallet was created under (the backend returns them in the request response), and its own paymasterApiKey.

app/approve-device/page.tsx
'use client';

import { useEffect, useState } from 'react';
import {
  CavosProvider,
  useCavos,
  HttpRecoveryClient,
  type CavosConfig,
} from '@cavos/kit';

const BACKEND_URL = 'https://services.cavos.xyz';

function Approve({ request }: { request: any }) {
  const { isAuthenticated, openModal, addSigner } = useCavos();
  const [done, setDone] = useState(false);

  const approve = async () => {
    // Signs add_signer(newSigner) gaslessly with THIS (registered) device's key.
    const { transactionHash } = await addSigner(request.newSigner);
    const recovery = new HttpRecoveryClient({
      baseUrl: BACKEND_URL,
      appId: request.appId,
    });
    await recovery.confirmDeviceAddition({ requestId: request.requestId, txHash: transactionHash });
    setDone(true);
  };

  if (done) return <p>Device approved.</p>;
  if (!isAuthenticated) return <button onClick={openModal}>Sign in to approve</button>;
  return <button onClick={approve}>Approve device</button>;
}

export default function ApproveDevicePage() {
  const [config, setConfig] = useState<CavosConfig | null>(null);
  const [request, setRequest] = useState<any>(null);

  useEffect(() => {
    const requestId = new URLSearchParams(window.location.search).get('request')!;
    // The GET returns appSalt, appId, network + the new device's pubkey,
    // so this page can rebuild the SAME wallet identity the owner registered.
    fetch(`${BACKEND_URL}/api/devices/request?id=${requestId}`)
      .then((r) => r.json())
      .then((data) => {
        if (!data.found) return;
        setRequest({
          requestId: data.request_id,
          appId: data.app_id,
          newSigner: { x: BigInt(data.new_pub_x), y: BigInt(data.new_pub_y) },
        });
        setConfig({
          // 'mainnet' or 'testnet' — map from the wallet's network.
          network: data.network === 'mainnet' ? 'mainnet' : 'testnet',
          appSalt: data.app_salt,
          appId: data.app_id,
          // YOUR paymaster key — never Cavos's.
          paymasterApiKey: process.env.NEXT_PUBLIC_CAVOS_PAYMASTER_API_KEY!,
          authBackendUrl: BACKEND_URL,
        });
      });
  }, []);

  if (!config || !request) return <p>Loading…</p>;
  return (
    <CavosProvider config={config} modal={{ appName: 'Your App' }}>
      <Approve request={request} />
    </CavosProvider>
  );
}

The approval page is the only place where a registered device authorizes a new one. Deploy it at a URL you control and set that URL as the app's device_approval_url in the Cavos dashboard.

The non-custodial guarantee

The backend can request, notify, and relay a device addition — it can never authorize one. The account contract gates add_signer so it only succeeds when called by the account itself, which requires a valid signature from an existing on-chain signer. The backend holds no keys and has no privileged contract role.

The newSigner pubkey in a pending request is only a proposal for the UI. The approving device's signature commits to the actual pubkey passed to add_signer, so tampering with the request produces an inert, unsigned record. Always show the user the device label and a pubkey fingerprint before they approve.

Encourage multiple devices early. If a user only ever has one device and loses it, they fall back to passphrase recovery. Adding a second device at onboarding makes recovery effortless.

See Concepts → non-custodial model for the full security rationale.

On this page