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.
-
Authentication only identifies the user. Google (or Apple, or an email code) produces a stable
userId. The login never signs transactions. -
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. -
The first
executedeploys. 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:
-
appSaltgoes 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 withvalidateCavosConfig(config). -
paymasterApiKeyonly belongs here when"starknet"is inchains. Putting it on a config that doesn't use Starknet is reported as a config error in development. -
socialRecovery: trueturns 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 needsappId, and the feature also has to be enabled for the app's environment in the dashboard. More on that below. -
deviceApprovalis"enclave"or"passkey". It already defaults to"enclave"whensocialRecoveryis 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. -
modalmounts 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: trueanddeviceApproval: "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
-
appSaltis 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.
-
networkis 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.
Top comments (0)