Cavos

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.

TypeScript
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

StatusMeaningCan execute?
readyThis device unwrapped the Ed25519 spend keyYes — if the account holds lamports
needs-device-approvalRegistry has an address, this device has no wrapNo — 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.

TypeScript
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.

TypeScript
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

TypeScript
const sig = await wallet.signMessage("Hello, Cavos!");
// sig.signature: Uint8Array (ed25519)
// sig.publicKey: base58 address
// sig.curve: "ed25519"

How keys work

  1. First device mints a random MasterDEK.
  2. Solana seed = HKDF-SHA256(DEK, appSalt, "cavos-ed25519-solana-v1").
  3. Address = Ed25519 public key of that seed. Registry claims (userId, appId, chain, network).
  4. 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".
  5. 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.

TypeScript
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
RouteNeedsWho is fee payer
'self' (default)SOL in the accountthe account itself
'sponsored'appId, or a relayerthe Cavos relayer
{ token }the account holds that tokensettled 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

TypeScript
import { SOLANA_NETWORKS } from "@cavos/kit";

SOLANA_NETWORKS["solana-devnet"];   // https://api.devnet.solana.com

There is no on-chain program and no SolanaAdapter: the account is a plain system account.

Recovery

PathWhat happens
EnclaveSign in again. The enclave unwraps the MasterDEK onto this device.
PasskeySign in. If a passkey was added, the vault asks for it ("Verify it's you") and opens its encrypted copy of the MasterDEK.
Recovery codeNot 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 / executeInstructions signed by the user key.
  • ✅ signMessage with curve: "ed25519".
  • ✅ Enclave DEK wrap and passkey restore.
  • ❌ PDA, secp256r1 precompile program, on-chain approvers, extra signers.

On this page