Dexter
Dexter
Docs
Wallet & identity

Verified against @dexterai/x402 5.3.1 · 2026-07-07

Sign in with Dexter

By the end of this page you will have a working "Sign in with Dexter" button, know the exact shape of the { session, vault } it hands back, and know every --dx-* CSS variable you can set to theme it.

The button runs a passkey ceremony. The user taps their face or fingerprint. Your app gets back an auth session plus their Dexter Wallet. The user keeps their own keys. This component is the front door; the vault engine is what sits behind it.

Install

npm install @dexterai/connect @dexterai/vault react

@dexterai/vault is a required peer. @solana/web3.js is an optional peer, needed only when you drive the passkey signer to open tabs.

The React surface lives at @dexterai/connect/react. The framework-free core (passkeyLogin, createWallet, the wallet store) lives at @dexterai/connect.

The button

Header.tsx
import { SignInWithDexter } from '@dexterai/connect/react';
 
export function Header() {
  return (
    <SignInWithDexter
      onSuccess={({ session, vault }) => {
        // session is always present; vault is present once the wallet is connected
        console.log(session.accessToken, session.expiresAt);
        if (vault) console.log(vault.swigAddress, vault.usdcAta);
      }}
      onError={(err) => console.error(err.code, err.message)}
    />
  );
}

One component, two states. Signed out, it renders the branded button. Signed in, it becomes a compact chip showing the Dexter Wallet address and the USD balance. That swap is built in — you do not render the chip yourself. Set showConnectedChip={false} if you want to render your own connected state instead.

What onSuccess hands you

onSuccess fires with a SignInResult:

interface SignInResult {
  session: PasskeyLoginTokens;   // always present
  vault?: ConnectVault;          // present once the wallet is connected
}

vault is optional in the type. Treat it as such — guard on it before reading vault fields. If you only need to seat an auth session, session alone is enough.

session (PasskeyLoginTokens) is the auth payload, camelCase:

FieldTypeMeaning
accessTokenstringBearer for your API calls
refreshTokenstringRefresh token
expiresAtnumberExpiry, unix seconds
expiresInnumberLifetime, seconds
tokenTypestringToken type

vault (ConnectVault) is the Dexter Wallet:

FieldTypeMeaning
swigAddressstringThe user-facing Dexter Wallet address (base58)
vaultPdastringVault PDA (base58) — needed to open tabs
receiveAddressstring | nullDeposit address; null until the wallet is deployed on-chain
usdcAtastring | nullUSDC token account for the balance read; null until deployed
publicKeystring33-byte SEC1 compressed P-256 authority key, base64 — needed to build the passkey signer
userHandlestringServer-minted wallet identity
credentialIdstringPasskey credential id

receiveAddress and usdcAta are null until the wallet's on-chain account is deployed (it deploys lazily on first use). Do not assume they are set right after sign-in.

Props

SignInWithDexterProps:

PropTypeDefaultWhat it does
onSuccess(result: SignInResult) => voidFires the moment sign-in completes
onError(error: ConnectError) => voidFires with the typed error on failure
labelstring"Sign in with Dexter"Signed-out button label
variant'primary' | 'secondary''primary'primary = filled ember, secondary = outline
blockbooleanfalseFull-width button
showConnectedChipbooleantrueRender the built-in connected chip
classNamestringExtra class, composed after the brand classes
apiBasestringhttps://api.dexter.cashdexter-api base
rpcUrlstringDexter's Helius proxyRPC for the connected-chip balance read

ConnectError carries a code (the server's snake_case error string) alongside message. Branch on code for typed handling.

Drive it yourself with the hook

If you want your own markup instead of the built-in button, use useSignInWithDexter. It is the same ceremony without the rendered surface.

CustomButton.tsx
import { useSignInWithDexter } from '@dexterai/connect/react';
 
export function CustomButton() {
  const dx = useSignInWithDexter();
 
  if (dx.isVaultConnected) {
    return (
      <button onClick={dx.disconnect}>
        {dx.vaultAddress} · {dx.usdcBalance === null ? '—' : `$${dx.usdcBalance.toFixed(2)}`}
      </button>
    );
  }
 
  return (
    <button disabled={dx.status === 'pending'} onClick={() => { void dx.signIn(); }}>
      {dx.status === 'pending' ? `${dx.phase}…` : 'Sign in with Dexter'}
    </button>
  );
}

The hook returns status ('idle' | 'pending' | 'done' | 'error'), the live phase ('challenge' | 'passkey' | 'verifying' | 'finalizing') while pending, signIn() / disconnect(), the resolved session / vault, convenience fields (vaultAddress, vaultPda, credentialId, usdcBalance, refreshBalance()), and the typed error.

signIn() resolves with the SignInResult and also throws ConnectError on failure — so you can handle it imperatively (try/catch) or declaratively (read status and error).

Create a new wallet

SignInWithDexter signs in an existing wallet. For the create flow — mint a brand-new passkey and vault — use CreateWalletPanel.

Onboard.tsx
import { CreateWalletPanel } from '@dexterai/connect/react';
 
export function Onboard() {
  return (
    <CreateWalletPanel
      transport="auto"
      showName
      onCreated={(result) => {
        console.log(result.handle, result.credentialId);
        console.log(result.vault.swigAddress);
      }}
      onError={(err) => console.error(err.code)}
    />
  );
}

onCreated fires with a CreateWalletResult: handle (base64url user handle), credentialId (base64url), and vault (a ConnectVault, with the swig not yet deployed — it deploys lazily). Props: transport ('auto' | 'popup' | 'inline', default 'auto'), showName (default true), apiBase, onError, className.

If you want the branded button without either turnkey flow — for example wired to your own create action — use DexterButton, which takes children, onClick, variant, block, loading plus loadingLabel, withMark, and disabled.

The transport flag

The passkey ceremony is bound to an origin. Off dexter.cash, an in-page WebAuthn call hits the rpId-origin problem, so the ceremony runs in a hosted popup instead.

  • 'auto' (default): inline on dexter.cash, popup on any other origin. This is what you want on a third-party site.
  • 'popup': always via the hosted popup on dexter.cash/connect. Works anywhere.
  • 'inline': always in-page. Only valid on a Dexter origin.

SignInWithDexter passes transport through the same way. The connected result is origin-checked and nonce-bound on both sides.

Open a tab from the connected wallet

Once connected, the hook exposes passkeySigner, a DexterApiBrowserPasskeySigner (or null before connect). It is what authorizes spends and opens x402 tabs.

import { useSignInWithDexter } from '@dexterai/connect/react';
import type { DexterApiBrowserPasskeySigner } from '@dexterai/vault/signers/browser';
 
export function useTabSigner() {
  const dx = useSignInWithDexter();
  const signer: DexterApiBrowserPasskeySigner | null = dx.passkeySigner;
  return signer; // drive it via signer.signOperation(opMessageBytes)
}

The signer's one method is signOperation(operationMessage). See the vault engine reference for the signer model and the operation messages it signs.

Theming: the --dx-* variables

The button, chip, and wallet kit read their colors and radius from CSS variables. Override them on any ancestor scope. Prefer this over className for color and shape. The audit that specced this page flagged that themeability was promised but never enumerated, so here is the complete set recoverable from the shipped dist.

VariableDefaultControls
--dx-ember#f26c18Primary accent. Start of the filled-button gradient, and (via color-mix) the borders, focus ring, and glow
--dx-ember-2#ba3a00End of the filled-button gradient (linear-gradient(135deg, --dx-ember, --dx-ember-2))
--dx-fg#fff4eaForeground text and glyph color on the button
--dx-danger#e5552eError-state text color
--dx-radius0pxCorner radius on the button and chip
theme.css
:root {
  --dx-ember: #6d28d9;    /* your brand's primary */
  --dx-ember-2: #4c1d95;  /* a darker shade for the gradient end */
  --dx-fg: #ffffff;
  --dx-radius: 8px;
}

These five are the whole set the shipped dist reads. There is no separate variable for the secondary (outline) variant's fill or for the chip background — both derive from --dx-ember through color-mix. If you need finer control than that, use className and target the brand classes directly.

On this page