cavos-react-sdk
Cavos React SDK — AI Agent Skill
Complete architectural knowledge and implementation patterns for
@cavos/react. This skill enables AI agents to correctly integrate, extend, and debug the Cavos SDK.
1. What is Cavos?
Cavos is a non-custodial account abstraction SDK for Starknet. It lets users create smart wallets using their existing OAuth identity (Google, Apple, Firebase email/password) — no seed phrases, no browser extensions.
Key Principles
- Non-custodial: The user's wallet is derived deterministically from their OAuth
subclaim + a per-app salt. No one holds the keys. - Gasless by default: All transactions go through the Cavos paymaster (SNIP-9 Outside Execution).
- Session keys: Ephemeral ECDSA keys that sign transactions on behalf of the user, with configurable spending limits and contract restrictions.
2. Architecture Overview
┌─────────────────────────────────────────────────────────┐
│ React App │
│ ┌───────────────────────────────────────────────────┐ │
│ │ <CavosProvider config={...}> │ │
│ │ useCavos() → { login, execute, address, ... } │ │
│ └───────────────────────────────────────────────────┘ │
│ │ │
│ ┌─────┴─────┐ │
│ │ CavosSDK │ │
│ └─────┬─────┘ │
│ ┌───────────────┼───────────────┐ │
│ ┌──────┴───────┐ ┌────┴──────┐ ┌──────┴──────┐ │
│ │ OAuthWallet │ │ Session │ │ Transaction │ │
│ │ Manager │ │ Manager │ │ Manager │ │
│ └──────────────┘ └───────────┘ └─────────────┘ │
│ Identity & Key Cavos Paymaster │
│ JWT handling lifecycle & SNIP-9 execution │
└─────────────────────────────────────────────────────────┘
│
┌─────┴─────┐
│ Starknet │
│ Contract │
│ (Cairo) │
└───────────┘
cavos.cairo
Three Layers
| Layer | Class | Responsibility |
|---|---|---|
| Identity | OAuthWalletManager |
OAuth login, JWT parsing, nonce computation, address derivation, session key generation, JWT signature building |
| Session | SessionManager |
Session key storage, expiration tracking, renewal detection |
| Execution | OAuthTransactionManager |
Account deployment, transaction execution (JWT vs session signature), session registration, renewal, revocation |
3. Configuration Reference
CavosConfig (Required)
interface CavosConfig {
appId: string; // From https://cavos.xyz/dashboard
backendUrl?: string; // Default: 'https://cavos.xyz'
starknetRpcUrl?: string; // Custom RPC (optional)
network?: 'mainnet' | 'sepolia'; // Default: 'sepolia'
paymasterApiKey?: string; // Cavos Paymaster API Key (optional)
enableLogging?: boolean; // Debug logs (default: false)
oauthWallet?: Partial<OAuthWalletConfig>; // Advanced: custom class hash, registry
session?: SessionConfig; // Session duration & default policy
}
SessionConfig
interface SessionConfig {
sessionDuration?: number; // Seconds (default: 86400 = 24h)
renewalGracePeriod?: number; // Seconds (default: 172800 = 48h)
defaultPolicy?: SessionKeyPolicy; // Applied to all sessions
}
SessionKeyPolicy ⚠️ Critical Type
interface SessionKeyPolicy {
spendingLimits: Array<{
token: string; // Contract address of the ERC-20 token
limit: bigint; // Maximum amount (in wei, use BigInt!)
}>;
allowedContracts: string[]; // Only these contracts can be called
maxCallsPerTx: number; // Max calls per multicall
}
[!CAUTION]
limitMUST be abigint(e.g.,BigInt(10 * 10**18)for 10 tokens with 18 decimals). Usingnumberwill silently truncate large values.
4. Complete API Reference
4.1 useCavos() Hook (Primary Interface)
This is what 99% of integrations should use:
const {
// --- State ---
isAuthenticated, // boolean
user, // UserInfo | null → { id, email, name, picture? }
address, // string | null → '0x...' Starknet address
hasActiveSession, // boolean
isLoading, // boolean — true during init/login
walletStatus, // WalletStatus → { isDeploying, isDeployed, isRegistering, isSessionActive, isReady, pendingDeployTxHash? }
sessionPublicKey, // string | null — public key of current session key (safe to display)
// --- Auth ---
login, // (provider: 'google'|'apple') => Promise<void>
sendMagicLink, // (email: string) => Promise<void> — passwordless; auth completes in background
logout, // () => Promise<void>
// --- Transactions ---
execute, // (calls: Call | Call[], options?: { gasless?: boolean }) => Promise<string>
// gasless: true (default) → Cavos Paymaster sponsors gas
// gasless: false → wallet pays gas from its own STRK balance
signMessage, // (typedData: TypedData) => Promise<string[]> (SNIP-12 → [SESSION_V1_magic, r, s, session_key])
// --- Session Management ---
registerCurrentSession, // () => Promise<string> → explicit on-chain registration
updateSessionPolicy, // (policy: SessionKeyPolicy) => void → MUST call before register!
renewSession, // () => Promise<string>
revokeSession, // (sessionKey: string) => Promise<string> — sessionKey is REQUIRED
emergencyRevokeAllSessions, // () => Promise<string> → nuclear option
exportSession, // () => string → base64 token for CLI
// --- Account ---
isAccountDeployed, // () => Promise<boolean>
deployAccount, // () => Promise<string>
getBalance, // () => Promise<string> → ETH balance as string (wei)
// --- Multi-Wallet ---
getAssociatedWallets, // () => Promise<{ address: string; name?: string }[]>
switchWallet, // (name?: string) => Promise<void>
// --- Modal ---
openModal, // () => void — open built-in auth modal
closeModal, // () => void — close built-in auth modal
// --- Utilities ---
getOnramp, // (provider: 'RAMP_NETWORK') => string → fiat onramp URL
resendVerificationEmail, // (email: string) => Promise<void>
// --- Raw SDK (Advanced) ---
cavos, // CavosSDK instance for direct access
} = useCavos();
4.2 Login Providers
| Provider | Method | Notes |
|---|---|---|
'google' |
login('google') |
Opens new tab → Google OAuth. Address derived from Google sub. |
'apple' |
login('apple') |
Opens new tab → Apple OAuth. Address derived from Apple sub. |
| Magic Link | sendMagicLink(email) |
Passwordless. Email sent immediately; auth completes in background when user clicks link. |
4.3 WalletStatus
interface WalletStatus {
isDeploying: boolean; // Account contract is being deployed
isDeployed: boolean; // Account contract is deployed on-chain
isRegistering: boolean; // Session key is being registered on-chain
isSessionActive: boolean; // Session key is registered and not expired
isReady: boolean; // Both deployed + session active — ready for transactions
pendingDeployTxHash?: string; // Set when deploy tx submitted but confirmation timed out
}
pendingDeployTxHash is useful to show an explorer link when deployment takes longer than expected (e.g., network congestion). The SDK persists this hash in localStorage and re-polls on next init().
5. Critical Flows & Rules
5.1 The Policy Synchronization Rule
[!IMPORTANT] ALWAYS call
updateSessionPolicy(policy)BEFOREregisterCurrentSession().
Why: The session policy is captured at login time. If the user changes their spending limits in the UI after login but before registration, the stale (possibly empty) policy gets stored on-chain.
What happens if you don't: The contract sees policy_count == 0 and skips all spending limit checks, allowing unlimited transfers.
// ✅ CORRECT
const activate = async () => {
updateSessionPolicy(latestPolicy); // Sync first!
await registerCurrentSession(); // Then register
};
// ❌ WRONG — stale policy gets registered
const activate = async () => {
await registerCurrentSession(); // Uses policy from login time!
};
See: Policy Synchronization Deep Dive
5.2 Transaction Execution Flow
execute(calls, { gasless? }) (gasless defaults to true)
│
├─ Session NOT registered?
│ ├─ gasless: true → JWT signature (OAUTH_JWT_V1), auto-registers + executes atomically
│ └─ gasless: false → THROWS — must register session first (via sponsored tx)
│
├─ Session registered & active?
│ ├─ gasless: true → Cavos Paymaster (SNIP-9 Outside Execution)
│ └─ gasless: false → Direct v3 INVOKE via raw RPC (wallet pays STRK)
│ • Custom fee estimation (dummy SESSION_V1 sig + 5M gas overhead for __validate__)
│ • Hash computed manually, submitted via starknet_addInvokeTransaction
│ • Bypasses starknet.js Account.execute() entirely
│
├─ Session expired but within grace period?
│ └─ Auto-renews session (always sponsored), then executes with chosen gasless option
│
└─ Session expired beyond grace period?
└─ Throws "SESSION_EXPIRED" → user must re-login
5.3 Account Deployment
Accounts are deployed automatically after login — no manual steps needed.
- After
login(), the SDK callsdeployAccountInBackground()which:- Deploys the account via Cavos Paymaster (gasless) if not already deployed
- Auto-registers the session key on-chain using JWT signature
- Updates
walletStatus.isReady = truewhen both steps complete
walletStatustransitions:isDeploying → isDeployed → isRegistering → isSessionActive → isReady ✅- No relayer needed — fully self-custodial.
5.4 Address Derivation
The wallet address is deterministic and derived from:
address = Poseidon(sub, salt, walletName?)
sub: OAuth subject claim (unique per user per provider)salt: Per-app salt fetched from the Cavos backendwalletName: Optional name for sub-accounts (default: unnamed)
5.5 Session Renewal
Sessions have two time boundaries:
valid_until: When the session expires (default: 24h after registration)renewal_deadline: Grace period window (default: 48h after expiry)
Between valid_until and renewal_deadline, the old session key can sign a renewal request for a new session key, without needing a new JWT.
5.6 Session Revocation
Two levels of revocation:
revokeSession(sessionKey: string): Invalidates one specific session key (thesessionKeyparameter is required — passsessionPublicKeyto revoke the current one). Requires JWT.emergencyRevokeAllSessions(): Increments the on-chainrevocation_epoch, invalidating ALL sessions. Nuclear option.
5.7 Magic Link Authentication
sendMagicLink(email) is a fire-and-forget method:
- Sends a magic link email via the Cavos backend (returns immediately)
- The SDK starts polling
localStorageforcavos_auth_result - When the user clicks the link (in any tab / device), the auth tab writes the result to localStorage and closes
- The SDK picks up the result via
storageevent or poll, callshandleCallback(), then firesonAuthChangelisteners - React state (
isAuthenticated,address,walletStatus) updates automatically
To react to magic link completion in UI, subscribe via cavos.onAuthChange(cb) or simply observe isAuthenticated changing.
6. Common Patterns
Minimal Integration (5 lines)
import { CavosProvider, useCavos } from '@cavos/react';
// In your layout:
<CavosProvider config={{ appId: 'YOUR_APP_ID', network: 'sepolia' }}>
<App />
</CavosProvider>
// In your component:
function App() {
const { login, execute, address, isAuthenticated } = useCavos();
if (!isAuthenticated) return <button onClick={() => login('google')}>Login</button>;
return <p>Wallet: {address}</p>;
}
Execute a Token Transfer
// Sponsored (default)
const tx = await execute({
contractAddress: '0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d', // STRK
entrypoint: 'transfer',
calldata: [recipientAddress, amountLow, amountHigh] // uint256 = [low, high]
});
// User pays gas
const tx = await execute(
{ contractAddress: '0x04718...', entrypoint: 'transfer', calldata: [...] },
{ gasless: false }
);
Multi-Call (Approve + Swap)
const tx = await execute([
{
contractAddress: TOKEN_ADDRESS,
entrypoint: 'approve',
calldata: [ROUTER_ADDRESS, amountLow, amountHigh]
},
{
contractAddress: ROUTER_ADDRESS,
entrypoint: 'swap',
calldata: [...]
}
]);
Export Session to CLI
const token = exportSession();
// User runs: cavos session import <token>
7. Troubleshooting
| Symptom | Likely Cause | Fix |
|---|---|---|
| "Spending limit exceeded" | BigInt conversion error — wrong decimals | Verify limit: BigInt(amount * 10**decimals) |
| Transfer goes through despite limit | policy_count == 0 on-chain |
Call updateSessionPolicy() before registerCurrentSession() |
| "Address seed mismatch" | Different sub or salt between login and verification |
Ensure appSalt is fetched correctly from backend |
| "SESSION_EXPIRED" | Session older than valid_until |
Call renewSession() if within grace period, else re-login |
JwtExpiredError thrown on execute() |
OAuth JWT expired (typically after 1h) | Re-login via login(provider) |
| "Claim mismatch after decoding" | JWT kid rotation or issuer mismatch | Check JWKS registry is up to date |
| Account not deploying | No ETH/STRK for gas | Use paymaster (default) or fund the counterfactual address |
useCavos throws "must be used within CavosProvider" |
Component is outside the provider tree | Wrap your app in <CavosProvider> |
| "non-sponsored transaction without a registered session" | gasless: false called before any on-chain session |
Execute one sponsored tx first, or call registerCurrentSession() |
"Out of gas" in __validate__ (user-pays path) |
SKIP_VALIDATE estimation doesn't include validation gas | Already handled: SDK adds 5M L2-gas overhead automatically |
| "Resource bounds not satisfied" (user-pays path) | l1_gas.max_price_per_unit=0 in submitted tx | Already handled: SDK reads current l1_gas_price from estimateFee response |
| Magic link auth never completes | localStorage blocked (private browsing / iframe) | Ensure app is not in a sandboxed iframe; magic link requires localStorage access |
revokeSession() throws "missing argument" |
sessionKey parameter is required |
Pass sessionPublicKey from useCavos(): revokeSession(sessionPublicKey!) |
pendingDeployTxHash never clears |
Deploy tx still unconfirmed | Link to explorer; SDK will re-poll on next page load |
8. Token Addresses (Starknet)
| Token | Mainnet | Sepolia |
|---|---|---|
| ETH | 0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 |
Same |
| STRK | 0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d |
Same |
| USDC | 0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8 |
— |
| USDT | 0x068f5c6a61780768455de69077e07e89787839bf8166decfbf92b645209c0fb8 |
— |
9. File Map (SDK Source)
When modifying the SDK, here's where things live:
| File | Purpose |
|---|---|
src/CavosSDK.ts |
Main orchestrator — all public methods |
src/react/CavosContext.tsx |
React provider & useCavos() hook |
src/oauth/OAuthWalletManager.ts |
Identity, JWT, session keys, signatures |
src/oauth/OAuthTransactionManager.ts |
Deployment, execution, renewal, revocation |
src/oauth/AddressSeedManager.ts |
Deterministic address computation |
src/oauth/NonceManager.ts |
Session nonce for JWT binding |
src/types/config.ts |
CavosConfig, SessionConfig, OAuthWalletConfig |
src/types/session.ts |
SessionKeyPolicy, SessionData |
src/types/auth.ts |
UserInfo, LoginProvider, FirebaseCredentials |
src/paymaster/PaymasterIntegration.ts |
Cavos paymaster wrapper |
src/config/defaults.ts |
Network-specific defaults (class hashes, registry addresses) |
10. Coding Rules for AI Agents
- Never expose private keys — use the managers, not raw crypto.
- Always sync policy before registration — see Section 5.1.
- When adding SDK methods: Expose in
CavosSDK.ts→CavosContext.tsx→ updateCavosContextValueinterface. - uint256 in calldata is always
[low, high]— two felts. - After SDK changes: Run
npm run buildinreact/, then copydist/to consumer'snode_modules/@cavos/react/dist/. - Session storage is
sessionStorage(cleared on tab close) — this is intentional for security. - Wallet names are currently stored in
localStorage(cavos_seen_wallets_${appId}_${sub}) — not persistent across devices. revokeSession(sessionKey)— thesessionKeyargument is required. UsesessionPublicKeyfromuseCavos()to revoke the current session.sendMagicLinkis fire-and-forget. Auth completes viaonAuthChangelisteners, not via a returned Promise. Don't await auth state in the same call chain.signMessagereturnsstring[], not a{ r, s }object. The array is[SESSION_V1_magic, r, s, session_key]— ready for on-chainis_valid_signature.pendingDeployTxHashinWalletStatusis set when a deploy tx was submitted but confirmation timed out. It persists in localStorage across page loads until the tx confirms.
For detailed reference documents, see the references/ directory. For runnable code examples, see the scripts/ directory.