Troubleshooting
Symptoms you are likely to hit integrating @cavos/kit, what actually causes them, and how to confirm it.
Organized by what you see, not by what is wrong — the cause is usually somewhere other than where the symptom appears.
Users suddenly have new, empty wallets
Almost always the userId changed. Addresses are looked up in the registry
by (userId, appId, chain, network). If your auth changes what it reports — an
email where it used to be a primary key, a provider migration that renumbers
users — those users land on a different wallet. Nothing errors: the app works
perfectly, on the wrong accounts.
The same applies to appId. If you switch environments or change the app ID,
users get new wallets. Derive userId from something immutable.
On Starknet, appSalt namespaces the local device key, not the
registry address. Changing it means this browser's P-256 key is not authorized
on the existing wallet (needs-device-approval).
On native Solana and Stellar, appSalt is also the HKDF salt for the spend
key. Changing it derives a different Ed25519 address that will not match the
registry row — the user cannot unwrap the DEK onto the claimed account. Treat
it as permanent.
redirect_uri is not registered for this app
Returned when the URL your app started OAuth from is not on the app's registered list — including when the app has no URLs registered at all. An empty list rejects rather than allows, because the callback carries a one-time code and honouring an unregistered URI would hand that code to whoever asked.
Add the exact URL in Dashboard → App → Callback URLs: scheme, host and path must match, with no trailing slash and no wildcards. Tunnel and preview domains each count as a separate origin, and a fresh tunnel URL means a fresh registration. See Authentication.
paymasterApiKey is required for Starknet connections
Starknet sponsors both the account deploy and every transaction through the
paymaster, so the key is required rather than optional. Solana and Stellar use a
relayer activated by appId instead — a paymaster key there is ignored, and the
development config check flags it as [unused-paymaster-key] so it does not sit
in your bundle for nothing.
Transactions fail with the wallet apparently connected
Check that status !== "needs-device-approval". Both undeployed and ready
wallets can execute — only needs-device-approval blocks it.
A user can be signed in on a device the account has not authorized yet — the
normal state on a second browser — and execute will fail until the device is
approved. Route walletStatus.needsDeviceApproval to a passkey prompt or
multi-device approval.
Don't require isReady for execution. An undeployed wallet can execute.
The first execution deploys the account atomically, then runs the operation.
Nothing happens when social recovery should run
Three causes, in the order worth checking:
- Not enabled for the environment.
socialRecovery: trueis the client half; the environment must also have it enabled in the dashboard, with one provider selected. It is off by default. - No
appId. The enclave session is scoped to the app; without it the feature cannot start. Flagged as[social-recovery-without-app-id]. - Your own auth, with no provider token supplied. If you authenticate users
yourself, enrolment and recovery do not happen automatically — the SDK never
sees a provider token. Call
submitSocialRecoveryToken(idToken)yourself. See With your own auth. - The creating device never sealed. Mac logged in from IndexedDB; the phone
got
not_enrolled. Reconnect once on the device that has the wrap — enroll is idempotent.
id_token issuer … cannot be used for social recovery
You passed your own session JWT rather than the provider's id_token. The
enclave verifies against Google's or Apple's published keys, and a token your
auth signed cannot be verified against them — so it is rejected before it is
sent anywhere.
Clerk, Auth0 and Firebase all expose the underlying provider token, usually behind an explicit call rather than as the default session object.
social authentication is too old; sign in again
The enclave requires the authentication to be under five minutes old, so a recovery cannot be driven by a token retrieved from an old session. Pass one straight from a sign-in. If your auth caches tokens, ask it for a fresh one rather than reusing what it has.
Errors log as {}
A host whose console JSON-serializes its arguments — notably Capacitor's native
bridge — prints an Error as {}, because message and stack are
non-enumerable. The value is not lost, just unprinted: read the message off the
error you caught, or off the status field for background work
(walletStatus-level errors and authError).
Hydration or window is not defined errors
The React bindings are client-only — @cavos/kit/react is marked
'use client'. In Next.js App Router, the file that renders <CavosProvider>
needs 'use client' at the top, and so does any component calling useCavos().
Wallet state is not available during server rendering by design: the device key
lives in the browser. Gate on isLoading for the first paint rather than
expecting an address to be present immediately.
A user is stuck mid-recovery with a timelock
Expected, and not stuck. A non-zero timelock commits the new signer on-chain but
keeps it inert until the delay passes — that window is what lets a real owner
cancel an unwanted recovery. The SDK persists the public readyAt, resumes
after a refresh, and finalizes automatically when the delay expires. Surface
walletStatus.socialRecoveryReadyAt so the user knows when to come back.
Reserve refused to sign, or token_not_allowed
That is @cavos/reserve, not the kit Stellar relayer. The SDK
compares the built transaction against the request you made and refuses to
sign on mismatch.
Typical causes:
maxSend/maxSendStroopsmissing or too tight (path_moved, orReserveErrorfrom the client before a request is sent).- Fee token not on this host's allowlist — call
reserve.tokens(). An asset code is not an identity; useCODE:ISSUER. - Calling
/v1/buildand signing the bytes instead ofsend. - Quote expired (~60 seconds) —
quote_expired. - Creating a new account while another bootstrap is in flight —
503 bootstrap_busy. Retry.
Kit wallet.execute on Stellar still uses the Cavos XLM relayer. Do not mix
the two paths for the same first-create.
Email sign-in spins forever, or fails with this wallet is protected by recovery
The app runs the enclave and the user signed in with an email code. A code
returns a Cavos-signed token, which the enclave never accepts, so a Solana or
Stellar wallet cannot be created or restored from it. Starknet still connects,
which makes it look like email half-works. Use the magic link instead: from
@cavos/kit 0.2.3 CavosAuthModal does so on its own whenever the app runs
the enclave. If you call sendOtp / verifyOtp yourself, switch to
sendMagicLink. See Authentication.
Before 0.2.3 the modal also hid this error: it stayed on "Connecting with…",
then "This is taking longer than usual", with the real message only in
authError. It now returns to the sign-in screen and shows the error, so
upgrade first if all you see is the spinner. The same release fixes the
connecting screen naming the provider used last time ("Connecting with Google")
instead of email.
If the magic link itself answers redirect_uri is not registered for this app,
register the page it was requested from; see
above.
Still stuck
Run the config checks and read what they say — they cover the mistakes above that are visible from configuration alone:
import { validateCavosConfig, formatConfigProblems } from "@cavos/kit/react";
console.log(formatConfigProblems(validateCavosConfig(config)));<CavosProvider> already runs these in development. Calling them yourself is
useful in a setup screen or in CI, where a misconfigured environment is cheaper
to catch than in a user's first login.
The Cavos vault
kit/vault: add <origin> to this app's allowed web origins in the Cavos dashboard.
The page's origin is not registered for the app, so the vault refuses to load.
Add it (scheme, host and port, e.g. http://localhost:3000) under the app's
settings. An app with no origins registered cannot embed the vault at all.
kit/vault: https://vault.cavos.xyz did not load. The iframe never
answered within 15 seconds. Check that nothing on your page blocks framing
vault.cavos.xyz (a Content Security Policy with frame-src or child-src
that omits it), and that the page is served over HTTPS or localhost.
Transfers that used to sign silently now ask, or fail with kit/vault: this app's policy does not allow this transaction. They are over the app's
limits, or are not a transfer of a listed token (contract calls, token
approvals, signer changes). Check the app's Approvals page. The daily total
counts only what was signed without asking, per user, and resets at 00:00 UTC.
A returning user lands on needs-device-approval after upgrading to 0.2.0.
Keys created before the vault lived in your page's storage, which the vault
cannot read. Solana and Stellar wallets restore through the enclave or the
passkey, as on a new device; Starknet adds this browser as a new device. See
Cavos vault.