Cavos

Concepts

The every-chain architecture, non-custodial invariant, registry-first address model, and lazy deployment.

Every-chain, not one account model everywhere

Cavos standardizes the integration contract, not the blockchain. Every adapter must provide stable identity, local user authority, typed execution, non-custodial recovery, and a documented sponsorship path. The implementation remains native to the selected network.

StarknetAdapter, SolanaAdapter, and StellarAdapter are available today. Future chains are roadmap adapters until their code, security review, and end-to-end validation are complete.

Non-custodial invariant

The Cavos backend must not possess authority that can move user funds or enroll itself as a signer.

  • Starknet: the Cairo account authorizes P-256 device signers.
  • Solana: the device-account program authorizes P-256 keys whose signatures were verified by the native precompile.
  • Stellar: the classic account authorizes an ed25519 control key that is encrypted on-chain and unwrapped locally.

Registries and recovery services coordinate public state or messages. Paymasters and relayers fund or submit transactions. None of those roles is a substitute for user authorization.

Address resolution: registry-first

The address is not derived purely from identity. It is looked up in the registry first, and only computed when the user has no existing wallet.

(userId, appId, chain, network) → registry lookup → address

When you call connect:

  1. Registry lookup: The kit asks the Cavos registry: does this user already have a wallet for this app and chain?
  2. If found: The existing address is returned. The device checks whether it is an authorized signer on-chain.
  3. If not found: The device computes a candidate address and claims it in the registry. The first device to register wins.

This design prevents a second wallet from being created silently when the same user connects from a different device. The second device sees the existing address and enters device approval instead of minting a duplicate.

Why not deterministic from identity? A purely deterministic address would let any device derive it, but then how would the account know which device is authorized? The address includes the first device's public key (directly on Solana/Starknet, indirectly on Stellar), which is why the registry records which address belongs to which user.

The appSalt parameter

appSalt is not the address name. It is the local device-key slot — a namespace that keeps device keys separate across different apps on the same device. The same user on the same device but different apps gets different device keys, which is correct: each app has its own wallet.

The registry key is (userId, appId, chain, network). The appSalt affects the device-key derivation, not the address derivation directly.

Lazy deployment

Connect never deploys. It resolves the address and checks on-chain status, but the account is not created until the first execute.

Connect resultWhat happens
User has no walletAddress is computed and claimed in registry. status: "undeployed".
User has a wallet, this device is authorizedstatus: "ready".
User has a wallet, this device is not authorizedstatus: "needs-device-approval".

An undeployed wallet can:

  • Sign messages (signMessage) — the signature comes from the local device key, which exists regardless of on-chain state.
  • Execute — the first execute call deploys the account and runs the user's operation atomically in a single sponsored transaction.

This lazy model means users see their address immediately and can share it before their first transaction. The account is created only when they use it.

Status values

Every wallet has a status property:

StatusMeaningCan execute?Can signMessage?
undeployedAddress derived, no on-chain account yetYes (first execute deploys)Yes
readyAccount deployed, this device authorizedYesYes
needs-device-approvalAccount deployed, this device not authorizedNoNo

The only status that blocks execution is needs-device-approval. Both undeployed and ready can execute.

Multi-chain sessions

One connect call can configure multiple chains. The session provides access to all of them without re-authenticating:

TypeScript
const session = await Cavos.connect({
  chains: ["solana", "stellar", "starknet"],
  defaultChain: "solana",
  network: "testnet",
  identity,
  appSalt: "my-app",
  appId,
  paymasterApiKey, // Starknet only
});

// The session IS the default chain's wallet
session.address;           // Solana address
session.status;            // Solana status

// Access other chains
session.wallet("stellar"); // CavosStellar instance
session.chainStatus("stellar");
session.chainAddress("stellar");

Each chain has its own address (derived from the same identity), its own deployment status, and its own device authorization state. They are independent accounts sharing one login.

Device-native authority

"Device-native" describes where sensitive authority is created or unlocked, not one universal curve:

  • Starknet and Solana use non-extractable P-256 device signers directly.
  • Stellar uses a device-bound P-256 unwrap key to recover the active ed25519 control key locally.

On mobile, Cavos prefers hardware-backed key storage and reports the actual capabilities of the device. Hardware claims must be validated on physical devices, not inferred from a simulator.

Network environments

The shared API accepts network: "testnet" | "mainnet" and the selected adapter resolves the concrete network:

TypeScript
const session = await Cavos.connect({
  chains: ["stellar"],
  defaultChain: "stellar",
  network: "testnet", // → stellar-testnet
  identity,
  appSalt: "my-app",
  appId,
});
networkStarknetSolanaStellar
testnetsepoliasolana-devnetstellar-testnet
mainnetmainnetsolana-mainnetstellar-mainnet

Chain packages also export concrete network constants for advanced use. See the individual chain guides.

Typed native execution

Cavos.connect returns a CavosWallet & CavosSession union. Narrow on wallet.chain before execution. This keeps onboarding portable without hiding important differences in calls, units, replay protection, or confirmation.

TypeScript
const wallet = session.wallet("solana");

if (wallet.chain === "starknet") {
  await wallet.execute(calls);                    // Call[]
} else if (wallet.chain === "solana") {
  await wallet.execute(1_000_000n, destination);  // lamports, base58
} else if (wallet.chain === "stellar") {
  await wallet.execute(1_000_000n, destination);  // stroops, G…
}

That same rule applies to every future adapter: extend the discriminated union, document capabilities, and preserve native semantics.

On this page