Use cases
Consumer passkey wallet

Consumer passkey wallet

You'll build a single-page React app that gives users a multi-chain wallet they can log into with FaceID, TouchID, or Windows Hello. There is no seed phrase to write down. The vault lives on Base; outbound transfers fan out to Optimism, Arbitrum, and other supported chains over Wormhole — your users never bridge manually.

What you ship

  • A "Sign up with passkey" button that provisions a vault on first use and recognises the user on every subsequent visit.
  • A balance view that shows native + ERC-20 balances on every supported chain in one panel.
  • A "Send" form that picks a target chain from a dropdown — the SDK handles the cross-chain message under the hood.

Prerequisites

  • A Vite / Next.js / CRA project (anything that serves over HTTPS or localhost).
  • npm install @veridex/sdk @veridex/react ethers

Passkeys + iframes don't mix. Browsers refuse to surface the FaceID/TouchID dialog inside cross-origin iframes. Always serve this wallet from a top-level origin (https://wallet.yourapp.com), and link out from any embed.

1. Set up the SDK provider

// app/providers.tsx
import { VeridexProvider } from '@veridex/react';
 
export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <VeridexProvider
      chain="base"
      network="testnet"
      relayerUrl="https://relayer.veridex.network"   // public relayer; no API key on testnet
    >
      {children}
    </VeridexProvider>
  );
}

The provider initialises one VeridexSDK per chain and exposes it through React context. Hooks (usePasskey, useVault, useBalances, useTransfer) read from that context — no prop drilling.

2. Sign up / sign in with a passkey

// app/components/LoginButton.tsx
import { usePasskey } from '@veridex/react';
 
export function LoginButton() {
  const { register, authenticate, credential, status } = usePasskey();
 
  if (credential) return <div>Signed in as {credential.label}</div>;
 
  return (
    <div className="flex gap-2">
      <button onClick={() => register('user@example.com', 'My Wallet')}>
        Sign up with passkey
      </button>
      <button onClick={authenticate}>Sign in</button>
      {status === 'pending' && <span>Prompting…</span>}
    </div>
  );
}

register() triggers navigator.credentials.create() — the browser shows the FaceID/TouchID dialog, mints a WebAuthn credential, and the SDK derives a deterministic vault address from the credential's public key. authenticate() re-binds an existing credential on returning visits, no re-registration needed.

3. Render a multi-chain balance panel

// app/components/Balances.tsx
import { useBalances } from '@veridex/react';
 
export function Balances() {
  const { balances, loading, refresh } = useBalances({
    chains: ['base', 'optimism', 'arbitrum'],         // hook fans out the RPC calls in parallel
    tokens: ['native', 'USDC'],
  });
 
  if (loading) return <div>Loading balances…</div>;
 
  return (
    <table>
      <thead><tr><th>Chain</th><th>Native</th><th>USDC</th></tr></thead>
      <tbody>
        {balances.map((row) => (
          <tr key={row.chain}>
            <td>{row.chain}</td>
            <td>{row.native.formatted}</td>
            <td>{row.usdc?.formatted ?? '—'}</td>
          </tr>
        ))}
      </tbody>
      <tfoot>
        <tr><td colSpan={3}><button onClick={refresh}>Refresh</button></td></tr>
      </tfoot>
    </table>
  );
}

The hook caches per chain and invalidates entries when a transfer settles, so the table updates without manual refetching after a Send.

4. Send across chains, no manual bridging

// app/components/SendForm.tsx
import { useTransfer } from '@veridex/react';
import { parseEther } from 'ethers';
 
const CHAINS = [
  { id: 10004, label: 'Base Sepolia' },
  { id: 10005, label: 'Optimism Sepolia' },
  { id: 10003, label: 'Arbitrum Sepolia' },
];
 
export function SendForm() {
  const { send, status, txHash, error } = useTransfer();
 
  return (
    <form
      onSubmit={async (e) => {
        e.preventDefault();
        const fd = new FormData(e.currentTarget);
        await send({
          targetChain: Number(fd.get('chain')),
          token: 'native',
          recipient: String(fd.get('to')),
          amount: parseEther(String(fd.get('amount'))),
        });
      }}
    >
      <select name="chain">
        {CHAINS.map((c) => <option key={c.id} value={c.id}>{c.label}</option>)}
      </select>
      <input name="to" placeholder="0x…" />
      <input name="amount" placeholder="0.01" />
      <button type="submit" disabled={status === 'sending'}>
        {status === 'sending' ? 'Sending…' : 'Send'}
      </button>
      {txHash && <a href={`https://sepolia.basescan.org/tx/${txHash}`} target="_blank" rel="noreferrer">View tx</a>}
      {error && <div className="text-red">{error.message}</div>}
    </form>
  );
}

Under the hood useTransfer calls sdk.transferViaRelayer({ targetChain, ... }). The relayer takes the signed action on the Base hub, dispatches the cross-chain message through Wormhole, and the destination spoke chain (Optimism, Arbitrum, …) materialises the funds. Your user sees one prompt, one confirmation, one transaction hash.

5. Recover on a new device

Passkeys sync through iCloud Keychain / Google Password Manager / 1Password. A user who lost their laptop signs in on their new device with FaceID and the same vault is there — there's no seed-phrase recovery flow because there's no seed phrase. For users without cloud-synced passkeys, see Recovery for the social-recovery and multi-device options.

Going further

  • Add session keys so power users don't get a passkey prompt for every transfer: Sessions.
  • Add spend limits so a stolen device can't drain the vault before the user revokes it: Spending Limits.
  • Move from testnet to mainnet by switching network and grabbing a relayer API key (opens in a new tab).