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).
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).
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
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:
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).
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).
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.
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)
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
const { setupRecovery } = useCavos();
const code = await setupRecovery(); // string — show this to the user ONCEThe 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:
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");| Field | Meaning |
|---|---|
signature | 64-byte signature (r‖s for secp256r1, ed25519 raw). |
publicKey | The 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.publicKeyis the uncompressed04‖x‖yhex. - Solana — signs
sha256("\x18Solana Signed Message:\n<len>\n" + message), matching the wallet-adaptersignMessageconvention.publicKeyis the 33-byte compressed P-256 hex. - Stellar — the signing key is the ed25519 control key (not P-256), so
curveis"ed25519"andpublicKeyis theG…control address.
Verifying off-chain (what a third party / backend does):
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):
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 devicesignature,nonce,resourceBounds,version). A relayer broadcasts it via the account'sinvokeFunction. - 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.