Most people who bounce off a web3 app never get past the "install a browser extension and write down 24 words" step. An embedded wallet removes that step: the user signs in with Google, gets an address, and can send a transaction without ever holding gas.

This post walks through wiring that up in a React app with @cavos/kit, an embedded wallet SDK that supports Starknet, Solana and Stellar. I'll use Starknet for the main example because it shows the most moving parts (a paymaster, a multicall, lazy deployment), and point out where Solana and Stellar differ.

By the end you'll have:

  • a provider that wraps your app
  • a sign-in button with Google login
  • a gasless transaction from a wallet that didn't exist on-chain a minute earlier
  • a recovery path so users don't lose access when they switch phones

What actually happens under the hood

It helps to have the model in your head before writing code, because it explains the API.

  1. Authentication only identifies the user. Google (or Apple, or an email code) produces a stable userId. The login never signs transactions.
  2. Connect derives an address. The kit looks up (userId, appId, chain) in the Cavos registry. If there's an entry, you get that address back. If not, this device computes one and claims it. Connect never deploys anything.
  3. The first execute deploys. On an undeployed account, the first transaction deploys the account and runs your operation atomically, in one sponsored transaction.

On Starknet, the account is the standard, upgradeable OpenZeppelin AccountUpgradeable (v3.0.0), i.e. an audited OpenZeppelin account contract, not a custom one. Its stark key is derived from a per-user MasterDEK that lives on the user's devices. There's no on-chain device list: a new device restores the MasterDEK off-chain, via a passkey or the recovery enclave, and derives the same key.

1. Install and get your keys

npm install @cavos/kit 

The React bindings ship in the same package under @cavos/kit/react.

Create an app in the Cavos dashboard and copy its App ID. For Starknet you also need a paymaster API key.

# .env.local NEXT_PUBLIC_CAVOS_APP_ID=your-app-id NEXT_PUBLIC_CAVOS_PAYMASTER_API_KEY=your-paymaster-key 

A note on that paymaster key: it ends up in browser code, so treat it as a client-visible, app-scoped sponsorship credential, not a secret. Before going to production, limit it by app, environment, origin, rate and spend in the sponsorship service.

While you're in the dashboard, register your OAuth callback URLs under Dashboard → App → Callback URLs. They're matched exactly (scheme, host, path, no trailing slash, no wildcards), so add http://localhost:3000 and any preview or tunnel domains you use. Google login will reject a redirect URI that isn't on the list, and this is the most common "why doesn't login work" moment.

2. Wrap your app in CavosProvider

// app/providers.tsx "use client"; import { CavosProvider } from "@cavos/kit/react"; export function Providers({ children }: { children: React.ReactNode }) { return ( <CavosProvider config={{ appId: process.env.NEXT_PUBLIC_CAVOS_APP_ID, chains: ["starknet"], defaultChain: "starknet", network: "testnet", // Starknet Sepolia appSalt: "my-app", paymasterApiKey: process.env.NEXT_PUBLIC_CAVOS_PAYMASTER_API_KEY, socialRecovery: true, deviceApproval: "enclave", }} modal={{ appName: "My App", theme: "light", providers: ["google", "email"], }} > {children} </CavosProvider> ); } 

A few of these fields deserve a sentence each:

  • appSalt goes into the key derivation. Pick it once and never change it. A changed salt derives a different key and address, which silently moves every existing user to a new, empty wallet. In development the provider warns you if the salt changed since the browser last connected, and you can run the same checks yourself with validateCavosConfig(config).
  • paymasterApiKey only belongs here when "starknet" is in chains. Putting it on a config that doesn't use Starknet is reported as a config error in development.
  • socialRecovery: true turns on hardware-isolated recovery: the enclave wraps the MasterDEK so a user who signs in again on a new device can get their wallet back. It needs appId, and the feature also has to be enabled for the app's environment in the dashboard. More on that below.
  • deviceApproval is "enclave" or "passkey". It already defaults to "enclave" when socialRecovery is on, so the line above is explicit rather than required. "passkey" only works with a single chain; the provider throws if you combine it with several.
  • modal mounts the built-in login modal. Omit it if you want to build your own UI.

If you're on Next.js App Router, render <Providers> in your root layout.

3. Add a sign-in button

useCavos() is the main hook. It gives you state and actions from anywhere under the provider.

"use client"; import { useCavos } from "@cavos/kit/react"; export function WalletButton() { const { address, isAuthenticated, isLoading, walletStatus, openModal, logout } = useCavos(); if (isLoading) return <p>Connecting…</p>; if (!isAuthenticated) { return <button onClick={openModal}>Sign in</button>; } return ( <div> <p>{address}</p> <p>{walletStatus.isUndeployed ? "Not deployed yet" : "Deployed"}</p> <button onClick={logout}>Sign out</button> </div> ); } 

openModal() opens the hosted flow with the providers you listed. If you'd rather skip the modal and put a "Continue with Google" button directly in your own UI, call login instead:

const { login } = useCavos(); <button onClick={() => login("google")}>Continue with Google</button>; 

login("google") and login("apple") start OAuth. For email there's sendOtp(email) / verifyOtp(email, code). One caveat from the docs: with the enclave setup (deviceApproval: "enclave"), the modal uses a magic link for email, because an email OTP doesn't yield a token the recovery enclave accepts.

The OAuth return trip

You don't need a separate callback page or any extra code for the Google redirect. When the browser comes back, the URL carries a short-lived one-time code (?cavos_auth_code=…). CavosProvider checks for it on mount, strips it from the address bar right away, and exchanges it for an identity before connecting the wallet. While that runs, isLoading is true and the modal shows its "connecting" state; if it fails, the message lands in authError.

Two things are on you:

  • The provider has to be mounted on the page Google redirects back to. That's automatic if it lives in your root layout.
  • That URL has to be registered in the dashboard (see step 1).

If you build your own login flow outside the provider, useCavos() also exposes handleCallback(authData, redirectUri?) for handling the code yourself, but with the provider in place you shouldn't need it.

logout() clears the persisted identity and session token. It doesn't sign the user out of Google, and the stored keys stay on the device, so the next login is silent.

Already have auth?

If you use Clerk, Auth0 or your own sessions, pass the signed-in user to the provider through the identity prop instead. Cavos login turns off: the modal isn't mounted and login() throws. Pass null while your auth loads or after sign-out, and use an immutable primary key as userId, not an email that can change. A different userId means a different wallet. The authentication section of the docs covers this setup.

4. Send a gasless transaction

On Starknet, the context's execute(calls) sends a multicall signed by the stark key. It's sponsored by default and returns { transactionHash }.

"use client"; import { useState } from "react"; import { useCavos } from "@cavos/kit/react"; const TOKEN = "0x..."; // ERC-20 contract address export function SendButton({ to }: { to: string }) { const { execute, walletStatus } = useCavos(); const [hash, setHash] = useState<string | null>(null); const [error, setError] = useState<string | null>(null); async function send() { setError(null); try { // u256 amounts are two felts: low, high const { transactionHash } = await execute([ { contractAddress: TOKEN, entrypoint: "transfer", calldata: [to, "1000", "0"] }, ]); setHash(transactionHash); } catch (e) { setError(e instanceof Error ? e.message : String(e)); } } if (walletStatus.needsDeviceApproval) { return <p>This device needs to be restored before it can sign.</p>; } return ( <div> <button onClick={send}>Send</button> {hash && <p>Tx: {hash}</p>} {error && <p>{error}</p>} </div> ); } 

The important bit is which states can execute:

Status Meaning Can execute?
undeployed Address derived, no on-chain account yet Yes, first execute deploys and runs atomically
ready This device holds the account key Yes
needs-device-approval The wallet exists, but this device holds no key No, restore first

So you don't need a separate "deploy wallet" step or button. A brand-new user can sign in and send their first transaction in the same session, and the paymaster covers both the deployment and the call.

For amounts, convert human input to base units by multiplying by 10 ** decimals. The docs list the decimals per chain as Solana 9, Stellar 7, and 18 for the Starknet fee token.

Solana and Stellar

The context's execute is Starknet-only. For Solana and Stellar, narrow on wallet.chain and call the wallet directly:

const { wallet } = useCavos(); if (wallet?.chain === "solana") { const signature = await wallet.execute(1_000_000n, recipient); // lamports } else if (wallet?.chain === "stellar") { const hash = await wallet.execute(1_000_000n, recipient); // stroops } 

On Solana the hosted relayer becomes the fee payer (arbitrary programs are allowlisted per app), and on Stellar it can sponsor reserves and fee-bump submission. Neither needs a paymaster key. If you configure several chains, setChain("stellar") switches the active wallet without re-authenticating.

5. Don't skip recovery

This is the part people leave for later and regret. The keys live on the user's devices and never leave them. If a user loses every device and nothing was set up, Cavos can't restore access, because no Cavos key can move the funds. So the way back has to exist before it's needed.

On Starknet (and Solana and native Stellar), recovery means getting the MasterDEK onto a new device, off-chain. There are two paths:

  • The enclave. With socialRecovery: true and deviceApproval: "enclave" (the config above), the provider enrolls after a fresh login and unwraps on a device that doesn't have the key. The user just signs in with Google again.
  • A passkey. Offer it after sign-up, never during OAuth. Sign-up doesn't prompt for one by default.
const { walletStatus, passkeySupported, enrollPasskeyDefault } = useCavos(); if (passkeySupported && !walletStatus.hasPasskey) { await enrollPasskeyDefault(); // one platform prompt (Face ID / Touch ID / PIN) } 

On a fresh device where the automatic path didn't run (for example, the user dismissed the prompt), walletStatus.needsDeviceApproval is true and you can restore from the passkey:

const { walletStatus, approveDeviceWithPasskey } = useCavos(); if (walletStatus.needsDeviceApproval) { await approveDeviceWithPasskey(); // restores the MasterDEK; not an on-chain approval } 

To decide whether to nudge a user, read walletStatus.recovery:

const { walletStatus } = useCavos(); if (walletStatus.isReady && !walletStatus.recovery.protected) { // show: "Add a passkey so you can sign in on a new device." } 

recovery.protected starts false and flips once its lookup returns, so treat it as "not known to be protected" for the first moments of a connect. Don't flash a scary warning on the first render.

Checklist before production

  • appSalt is final and committed.
  • Every production and preview callback URL is registered in the dashboard.
  • The paymaster key is scoped and limited by origin, rate and spend.
  • network is switched from "testnet" to "mainnet".
  • Users are prompted to set up recovery once they're signed in.

Wrapping up

The whole integration is a provider, one hook, and a decision about recovery. Users sign in with an account they already have, the first transaction deploys their wallet without them noticing, and nobody needs to buy gas to try your app.

If you want to go further:

  • The Cavos docs cover per-chain configuration, signing messages and signing without submitting, and React Native.
  • How to add an embedded wallet is a longer guide that also covers the registry model and the "bring your own auth" setup.
  • The embedded wallet page has the product overview across Starknet, Solana and Stellar.