Solana
Use @cavos/kit on Solana — a native Ed25519 system account whose spend key is HKDF of a MasterDEK. The account pays its own fee, your relayer pays it, or the user pays in a token they hold.
Solana in @cavos/kit is a native system account. The address is an
Ed25519 public key — the same shape as Phantom — not a PDA and not a P-256
program account.
The spend key is HKDF of a MasterDEK (cavos-ed25519-solana-v1, salted by
appSalt). The same DEK derives the Stellar G… and the Starknet stark key in
a multi-chain enclave session. A new device restores that DEK with the enclave
or a passkey the user added. Signing up never asks for a passkey.
The account signs its own transactions. Who is named fee payer is a per-call choice — itself, the Cavos relayer, or the fee settled in a token the user already holds. None of them can spend: a co-signer cannot alter the instructions it signs.
Connect
Use Cavos.connect with chains: ["solana"]. An appId activates the
relayer.
Connect resolves the address and unwraps the spend key on this device. It does
not airdrop SOL and there is no initialize instruction.
import { Cavos } from "@cavos/kit";
const session = await Cavos.connect({
chains: ["solana"],
defaultChain: "solana",
network: "testnet", // "testnet" (devnet) | "mainnet"
appSalt: "my-app", // HKDF salt — never change it
identity: {
userId: user.id,
email: user.email,
},
appId: process.env.NEXT_PUBLIC_CAVOS_APP_ID,
});
const wallet = session.wallet("solana");
console.log(wallet.address); // base58 Ed25519 system account
console.log(wallet.status); // "ready" | "needs-device-approval"network: "testnet" resolves to solana-devnet; "mainnet" resolves to
solana-mainnet. Narrow on wallet.chain before chain-native calls.
Pass your own RPC on mainnet. Without rpcUrl / rpcUrls.solana, the kit
falls back to public api.mainnet-beta.solana.com, which is rate-limited.
Provide Helius / Triton / QuickNode in production.
Status values
| Status | Meaning | Can execute? |
|---|---|---|
ready | This device unwrapped the Ed25519 spend key | Yes — if the account holds lamports |
needs-device-approval | Registry has an address, this device has no wrap | No — restore with enclave login or approveDeviceWithPasskey() |
Native Solana does not use undeployed the way Starknet does. There is nothing
to deploy: a system account appears when it first receives lamports. After
connect, isDeployed is true once the spend key is present.
Fund the address, then spend. By default the account pays its own fee, so
it needs SOL. Send devnet SOL to wallet.address, or pick a fee route that
does not need it — see below. No route puts SOL in the account; sponsorship
covers the fee, never the balance.
Execute a transfer
execute(amount, destination) moves amount lamports from this system
account. The user's Ed25519 key signs the Solana message; the relayer is fee
payer.
if (wallet.chain === "solana" && wallet.status !== "needs-device-approval") {
const signature = await wallet.execute(
1_000_000n, // 0.001 SOL, in lamports
recipientPublicKey, // base58
);
console.log(signature);
}amount is a bigint of lamports (1 SOL = 1_000_000_000 lamports).
Execute arbitrary instructions
executeInstructions submits the user's signed transaction. The account is a
normal signer (owner of ATAs, etc.), not a PDA that CPIs on the user's behalf.
import { createTransferInstruction, getAssociatedTokenAddress } from "@solana/spl-token";
import type { InstructionData } from "@cavos/kit";
const source = await getAssociatedTokenAddress(usdcMint, new PublicKey(wallet.address));
const dest = await getAssociatedTokenAddress(usdcMint, recipientPublicKey);
const ix = createTransferInstruction(source, dest, new PublicKey(wallet.address), amount);
const instructions: InstructionData[] = [{
programId: ix.programId.toBase58(),
accounts: ix.keys.map((k) => ({
pubkey: k.pubkey.toBase58(),
isSigner: k.isSigner,
isWritable: k.isWritable,
})),
data: ix.data,
}];
if (wallet.chain === "solana" && wallet.status !== "needs-device-approval") {
await wallet.executeInstructions(instructions);
}Sponsorship is allowlisted. The relayer only co-signs instructions whose program ids are in the app's Solana program allowlist (dashboard → Solana Programs) plus System, SPL Token, Token-2022, and Associated Token. A 1,000,000 CU cap also applies.
A native account has one spend key. Restore it; do not add a second signer.
Sign messages
const sig = await wallet.signMessage("Hello, Cavos!");
// sig.signature: Uint8Array (ed25519)
// sig.publicKey: base58 address
// sig.curve: "ed25519"How keys work
- First device mints a random MasterDEK.
- Solana seed = HKDF-SHA256(DEK, appSalt,
"cavos-ed25519-solana-v1"). - Address = Ed25519 public key of that seed. Registry claims
(userId, appId, chain, network). - The local wrap of the DEK and the spend key live in the IndexedDB of the
Cavos vault on the web, filed under your app. The enclave
also seals the DEK when
deviceApproval: "enclave". - A reconnect that already has the local wrap reseals the enclave so a phone can recover even if the first enroll failed.
appSalt is part of the HKDF. Changing it derives a different address that will
not match the registry row.
Who pays the fee
fee answers one question — who pays, and in what — so the three answers are
one field rather than flags that look independent and are not.
await wallet.execute(amount, dest); // the account pays, in SOL
await wallet.execute(amount, dest, { fee: 'sponsored' }); // the Cavos relayer pays
await wallet.execute(amount, dest, { fee: { token: USDC } }); // the user pays, in USDC| Route | Needs | Who is fee payer |
|---|---|---|
'self' (default) | SOL in the account | the account itself |
'sponsored' | appId, or a relayer | the Cavos relayer |
{ token } | the account holds that token | settled in that token, same transaction |
'self' is the default here and not on Starknet or Stellar, where a fresh
account cannot deploy itself or meet the base reserve without help. A Solana
system account is signer and fee payer at once, so there was never a reason it
could not pay its own way.
No co-signer ever becomes the source of a user transfer.
sponsored: true / false still works as a deprecated alias for
fee: 'sponsored' / 'self'. fee wins if both are passed.
Networks
import { SOLANA_NETWORKS } from "@cavos/kit";
SOLANA_NETWORKS["solana-devnet"]; // https://api.devnet.solana.comThere is no on-chain program and no SolanaAdapter: the account is a plain system account.
Recovery
| Path | What happens |
|---|---|
| Enclave | Sign in again. The enclave unwraps the MasterDEK onto this device. |
| Passkey | Sign in. If a passkey was added, the vault asks for it ("Verify it's you") and opens its encrypted copy of the MasterDEK. |
| Recovery code | Not used. There is no recovery code on Solana. |
See Hardware-isolated recovery and Passkeys. Signing up never prompts for a passkey.
What's available
- ✅ Native Ed25519 system account.
- ✅ Three fee routes: the account itself, the relayer, or a token it holds.
- ✅
execute/executeInstructionssigned by the user key. - ✅
signMessagewithcurve: "ed25519". - ✅ Enclave DEK wrap and passkey restore.
- ❌ PDA, secp256r1 precompile program, on-chain approvers, extra signers.
Starknet
Use @cavos/kit on Starknet — a native OpenZeppelin account whose stark key is HKDF of a MasterDEK, with gasless execution via the Cavos paymaster.
Stellar
Use @cavos/kit on Stellar — a classic G… whose spend key is HKDF of a MasterDEK (same wrap as Solana). Relayer sponsors reserves and fees. Soroban and Trustless Work still work.