Cavos

After sign-in

Read balances, send native tokens, fund from a faucet, and manage security once the wallet is connected.

Once walletStatus.isReady is true, the wallet is deployed and this device is an authorized signer. This page covers the common things you do next — all of it via useCavos() inside a <CavosProvider>. See React for the provider/hook setup.

Every chain-specific call narrows on wallet.chain first, because execute keeps each chain's native signature. See Chains.

Read the native balance

The kit exposes each chain's balance read differently.

Solana

CavosSolana exposes the underlying @solana/web3.js Connection, so you read SOL directly. Balance is in lamports (1 SOL = 10⁹ lamports).

TypeScript
const { wallet, address } = useCavos();
if (wallet?.chain === "solana") {
  const lamports = await wallet.connection.getBalance(address); // number
  const sol = lamports / 1e9;
}

Stellar

CavosStellar has a balance() method. It returns native XLM in stroops (1 XLM = 10⁷ stroops).

TypeScript
if (wallet?.chain === "stellar") {
  const stroops = await wallet.balance(); // bigint
  const xlm = Number(stroops) / 1e7;
}

Starknet

There is no single "native balance" call in the kit for Starknet (the fee token is an ERC-20). Read a specific token with a balanceOf call via execute, or query the RPC directly with starknet.js.

Fund from a testnet faucet

Only on testnet. Handy for demos and first-run UX.

Solana — airdrop

TypeScript
if (wallet?.chain === "solana") {
  const sig = await wallet.connection.requestAirdrop(address, 1_000_000_000); // 1 SOL
  // confirm with connection.getSignatureStatus(sig) if you need to wait
}

Public Solana devnet rate-limits airdrops. For reliable funding, use a private RPC (Helius/Alchemy) as your rpcUrl, or the official Solana faucet.

Stellar — friendbot

Stellar testnet is funded by the public friendbot:

TypeScript
await fetch(`https://friendbot.stellar.org?addr=${encodeURIComponent(address)}`);

Send native tokens

Solana — SOL transfer

CavosSolana.execute(amount, destination) transfers SOL, sponsored by the relayer when appId is set. amount is in lamports (bigint).

TypeScript
if (wallet?.chain === "solana") {
  const txHash = await wallet.execute(1_000_000_000n, destinationAddress); // 1 SOL
}

Stellar — XLM transfer

Same signature, amount in stroops (bigint).

TypeScript
if (wallet?.chain === "stellar") {
  const txHash = await wallet.execute(10_000_0000n, destinationAddress); // 1 XLM
}

Both return the transaction hash as a string. Build explorer links per chain — Solana: https://explorer.solana.com/tx/${hash}?cluster=devnet; Stellar testnet: https://stellar.expert/explorer/testnet/tx/${hash}.

Starknet — arbitrary calls

On Starknet, use the context's execute(calls) for a multicall (transfers, swaps, any contract interaction). It's sponsored by default.

TypeScript
const { execute } = useCavos();
const { transactionHash } = await execute([
  { contractAddress: TOKEN, entrypoint: "transfer", calldata: [to, amountLow, amountHigh] },
]);

Formatting amounts? Parse a human input ("1.5") to base units by multiplying by 10 ** decimals and rounding; format a base-unit bigint back by dividing. Decimals per chain: Solana 9, Stellar 7, Starknet fee token 18.

Security after sign-in

These are chain-agnostic wrappers on useCavos() — they work identically on all three chains. They are the same primitives documented in Passkeys and Recovery, surfaced through the React hook.

Passkey (2FA for new devices)

TypeScript
const { walletStatus, enrollPasskeyDefault, passkeySupported } = useCavos();

if (passkeySupported && !walletStatus.hasPasskey) {
  await enrollPasskeyDefault(); // prompts the platform authenticator once
}

walletStatus.hasPasskey flips to true once enrolled.

Recovery code

TypeScript
const { setupRecovery } = useCavos();

const code = await setupRecovery(); // string — show this to the user ONCE

The recovery code is returned once. Display it immediately and tell the user to store it somewhere safe. Only its derived public key is registered on-chain — the code itself never leaves the device.

Approve a new device

When a user signs in on a fresh device, walletStatus.needsDeviceApproval is true and they can't transact yet. If they have a synced passkey:

TypeScript
const { walletStatus, approveDeviceWithPasskey } = useCavos();

if (walletStatus.needsDeviceApproval) {
  await approveDeviceWithPasskey(); // one passkey prompt authorizes this device
}

See Multi-device for the full device-approval flow.

Sign messages & transactions

Beyond sending transactions, a wallet needs to sign data off-chain (prove ownership without moving funds) and sign transactions without submitting (hand them to your own relayer). Both are available on every chain.

signMessage — off-chain signature

Sign an arbitrary message with the wallet's signing key. Nothing is submitted, no gas is paid, no state changes. Returns a uniform MessageSignature:

const { signMessage } = useCavos();               // or wallet.signMessage(...)
const { signature, publicKey, curve } = await signMessage("Sign in to dapp.com");
FieldMeaning
signature64-byte signature (r‖s for secp256r1, ed25519 raw).
publicKeyThe verifier input — Starknet/Solana: the device P-256 key (hex); Stellar: the G… control address.
curve"secp256r1" (Starknet, Solana) or "ed25519" (Stellar). Determines how to verify.

Per-chain details:

  • Starknet — signs sha256("Cavos Signed Message:\n<len>\n" + message) with the device P-256 key. publicKey is the uncompressed 04‖x‖y hex.
  • Solana — signs sha256("\x18Solana Signed Message:\n<len>\n" + message), matching the wallet-adapter signMessage convention. publicKey is the 33-byte compressed P-256 hex.
  • Stellar — the signing key is the ed25519 control key (not P-256), so curve is "ed25519" and publicKey is the G… control address.

Verifying off-chain (what a third party / backend does):

TypeScript
import { p256 } from "@noble/curves/p256";
import { sha256 } from "@noble/hashes/sha256";
import { Keypair } from "@stellar/stellar-sdk";

if (curve === "secp256r1") {
  // Starknet / Solana — verify the P-256 signature over sha256(prefixedMessage).
  const valid = p256.verify(signature, sha256(prefixedMessage), publicKeyHex);
} else {
  // Stellar — ed25519 verify against the control address.
  const kp = Keypair.fromPublicKey(publicKey); // G… address
  const valid = kp.verify(prefixedMessage, signature);
}

The verifier must reconstruct the same prefixed message the signer used (see the prefix per chain above). For Sign-In-with-X flows, embed a nonce + domain in the message string.

signTransaction — sign without submit

Sign a transaction WITHOUT broadcasting it. Each chain returns its own signed artifact (narrow on chain):

TypeScript
const { wallet } = useCavos();
if (wallet?.chain === "starknet") {
  const signed = await wallet.signTransaction(calls);   // → { calldata, signature, nonce, resourceBounds, version }
} else if (wallet?.chain === "stellar") {
  const signed = await wallet.signTransaction(amount, destination);  // → { xdr }
} else if (wallet?.chain === "solana") {
  const signed = await wallet.signTransaction(amount, destination);  // → { message, signature, publicKey }
}

What you get back, and what to do with it:

  • Starknet — the signed invoke (calldata, the 5-felt device signature, nonce, resourceBounds, version). A relayer broadcasts it via the account's invokeFunction.
  • Stellar — the control-signed inner transaction as base64 XDR. Submit it directly via Horizon, or hand it to a relayer to fee-bump (the control signature stays valid through the wrap).
  • Solana — the device's P-256 signature over the transfer message: { message, signature, publicKey }.

Solana's signTransaction is NOT a signed Solana transaction. The device P-256 key never signs the Solana transaction itself — it signs a domain-tagged message verified on-chain by the native secp256r1 precompile. A relayer or feePayer must take this (message, signature, publicKey) triple, assemble the [secp256r1 precompile ix, execute_transfer ix] bundle, add a recent blockhash

  • the feePayer's signature, and submit.

All signTransaction outputs are single-use and bind to on-chain state. Starknet binds to the nonce + resource bounds; Stellar to the sequence number (and a 180s timeout); Solana to the on-chain account nonce. If any other transaction from the same account is submitted first, the signature becomes invalid. Broadcast promptly.

On this page