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.
Each chain ships as a native client: CavosStarknet (OpenZeppelin account),
CavosSolana (Ed25519 system account), and CavosStellar (classic account).
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 audited OpenZeppelin
AccountUpgradeablev3.0.0, controlled by a stark key derived from the MasterDEK. - Solana: a native Ed25519 system account. The relayer is fee payer, never a spender.
- Stellar: the classic account's Ed25519 control key (MasterDEK, or extra Horizon signers on grandfathered wallets).
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 → addressWhen you call connect:
- Registry lookup: The kit asks the Cavos registry: does this user already have a wallet for this app and chain?
- If found: The existing address is returned. The device unwraps
the MasterDEK (local wrap, enclave, or passkey) or lands on
needs-device-approval. - If not found: The device mints a random MasterDEK, derives the address, and claims it. 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 lands on needs-device-approval until it restores the MasterDEK
— it does not mint a duplicate.
Why a registry? The first device mints a MasterDEK and claims the derived address. A second device must unwrap that same DEK — it cannot invent a new one. The registry records which address belongs to which user so connect does not mint a duplicate.
The appSalt parameter
appSalt is the local device-key slot. On Solana and Stellar native it is
also the HKDF salt for the spend key, so changing it derives a different
address that will not match the existing registry row. Never change it.
The registry key is (userId, appId, chain, network).
Lazy deployment
Connect never deploys Starknet or Stellar accounts. It resolves the address
and checks on-chain status; those accounts are created on the first execute.
Solana is a system account: ready once this device has the spend key.
Fund it, then spend — there is no initialize.
| Connect result | What happens |
|---|---|
| User has no wallet | Address is computed and claimed in registry. Starknet/Stellar: undeployed. Solana: ready once the spend key is unwrapped. |
| User has a wallet, this device can sign | status: "ready". |
| User has a wallet, this device holds no key | status: "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
executecall 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:
| Status | Meaning | Can execute? | Can signMessage? |
|---|---|---|---|
undeployed | Address derived, no on-chain account yet | Yes (first execute deploys) | Yes |
ready | Account deployed, this device holds the key | Yes | Yes |
needs-device-approval | This device holds no key yet; restore the MasterDEK | No | No |
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:
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 whether this device holds that chain's key. 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, Solana, and Stellar derive their account keys from a MasterDEK (enclave wrap or a passkey's encrypted copy): a stark key on Starknet, Ed25519 on Solana and Stellar. Grandfathered Stellar wallets still use extra Horizon signers.
- WebCrypto cannot hold a stark key, so outside the Cavos vault the Starknet key is re-derived into memory for the session; the DEK at rest stays protected by the device's non-extractable key.
- On the web, all of these keys live in the Cavos vault, an iframe on a Cavos origin, and never in your page.
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:
const session = await Cavos.connect({
chains: ["stellar"],
defaultChain: "stellar",
network: "testnet", // → stellar-testnet
identity,
appSalt: "my-app",
appId,
});network | Starknet | Solana | Stellar |
|---|---|---|---|
testnet | sepolia | solana-devnet | stellar-testnet |
mainnet | mainnet | solana-mainnet | stellar-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.
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.