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:
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:
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:
- Fetch the pending request to read the new device's public key.
- Approve it by calling
cavos.addSigner(newSigner)— this submits the on-chainadd_signercall, gasless via your paymaster key. - 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.
'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.