# Tempo Accounts SDK Tempo Accounts SDK documentation # Getting Started ## Overview The Tempo Accounts SDK is a TypeScript library for applications and wallets to create, manage, and interact with accounts on Tempo. ### Demo Below is a live example of the Tempo Wallet adapter from the Tempo Accounts SDK — connect a Tempo account and send a payment to see the end-to-end flow. } className="flex flex-col gap-3"> } /> ### Quick Prompt You can integrate the Tempo Accounts SDK, and achieve a flow like the demo above by prompting your agent: ``` Read accounts.tempo.xyz/docs and integrate Tempo accounts into my application ``` ### Adapters The Tempo Accounts SDK ships with several signing adapters. Pick the one that matches how you want to custody your Tempo account. ## Install The Tempo Accounts SDK is available as an [NPM package](https://www.npmjs.com/package/accounts) under `accounts` :::code-group ```bash [npm] npm i accounts ``` ```bash [pnpm] pnpm i accounts ``` ```bash [bun] bun i accounts ``` ::: ## Wagmi Usage The Tempo Accounts SDK is best used in conjunction with [Wagmi](https://wagmi.sh/) to provide a seamless experience for developers and end-users. ::::steps ### Set up Wagmi Get started with Wagmi by following the [official guide](https://wagmi.sh/react/getting-started). ### Configure After you have set up Wagmi, you can configure the Tempo Accounts SDK with `tempoWallet` from `wagmi/connectors` or `webAuthn` from `wagmi/tempo`. :::code-group ```tsx twoslash [Tempo Wallet] import { createConfig, http } from 'wagmi' import { tempo } from 'wagmi/chains' import { tempoWallet } from 'wagmi/connectors' // [!code hl] export const wagmiConfig = createConfig({ chains: [tempo], connectors: [tempoWallet()], // [!code hl] transports: { [tempo.id]: http(), }, }) ``` ```tsx twoslash [Domain-bound Passkeys] import { createConfig, http } from 'wagmi' import { tempo } from 'wagmi/chains' import { webAuthn } from 'wagmi/tempo' // [!code hl] export const wagmiConfig = createConfig({ chains: [tempo], connectors: [webAuthn({ authUrl: '/auth' })], // [!code hl] transports: { [tempo.id]: http(), }, }) ``` ::: :::info\[What connector should I use?] Most apps should use **Tempo Wallet** - it provides a universal account experience with embedded onramp, access keys, and transaction orchestration. **Domain-bound Passkeys** is for apps that want to manage domain-bound passkey accounts directly. [Learn more](/docs/faq#what-connector-should-i-use). ::: :::tip If you are using a wallet connection library and cannot supply a custom connector, you can use `Provider.create()` to create a new provider instance, and inject itself into the wallet connection library via [EIP-6963](https://eips.ethereum.org/EIPS/eip-6963). ```tsx import { Provider } from 'accounts' Provider.create() ``` ::: ### Use Accounts You can now use [Wagmi Hooks](https://wagmi.sh/react/api/hooks) like `useConnect`, or [Tempo Hooks](https://wagmi.sh/tempo) like `useTransfer`. ```tsx twoslash // @noErrors import { useConnect, useConnectors } from 'wagmi' function Connect() { const connect = useConnect() const connectors = useConnectors() return connectors?.map((connector) => ( )) } ``` ### Next Steps :::: ## Vanilla + Viem Usage You can get started with the Tempo Accounts SDK by creating a new `Provider` instance. Once set up, you can use the provider to interact with accounts via JSON-RPC, or use [Viem Actions](https://viem.sh/tempo) with `.getClient(){:js}`. ```tsx twoslash // @noErrors import { Provider } from 'accounts' import { parseUnits } from 'viem' import { tempoActions } from 'viem/tempo' const provider = Provider.create() const { accounts } = await provider.request({ method: 'wallet_connect', }) const client = provider.getClient().extend(tempoActions()) const { receipt } = await client.token.transferSync({ token: '0x20c0000000000000000000000000000000000001', to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb', amount: parseUnits('10', 6), }) ``` ## Examples Check out these examples to get started with the Tempo Accounts SDK. | Example | Description | | | ---------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | --------------------------------------- | | [Embed Tempo Wallet](https://github.com/tempoxyz/accounts/tree/main/examples/basic) | Wagmi-based setup using the `tempoWallet` connector to connect to Tempo Wallets. | [Guide](/docs/guides/connect-accounts) | | [Deposits](https://github.com/tempoxyz/accounts/tree/main/examples/deposits) | Open the deposit UI from a connected account. | [Guide](/docs/guides/deposits) | | [Embed Domain-bound Passkeys](https://github.com/tempoxyz/accounts/tree/main/examples/webauthn) | Domain-bound passkey example using Wagmi and the `webAuthn` connector. | [Guide](/docs/guides/authentication) | | [CLI + Tempo Wallets](https://github.com/tempoxyz/accounts/tree/main/examples/cli) | Minimal CLI setup to connect and authorize local keys using Tempo Wallets. | [Guide](/docs/guides/cli) | | [Access Keys + Tempo Wallets](https://github.com/tempoxyz/accounts/tree/main/examples/access-key) | Authorize access keys using Tempo Wallets to submit transactions without prompts. | [Guide](/docs/guides/spend-permissions) | | [Access Keys + Domain-bound Passkeys](https://github.com/tempoxyz/accounts/tree/main/examples/access-key-and-webauthn) | Authorize access keys using domain-bound Passkeys. | [Guide](/docs/guides/spend-permissions) | | [Sponsor Fees + Tempo Wallets](https://github.com/tempoxyz/accounts/tree/main/examples/fee-payer) | Sponsor transactions via Tempo Wallets. | [Guide](/docs/guides/fee-sponsorship) | | [Sponsor Fees + Domain-bound Passkeys](https://github.com/tempoxyz/accounts/tree/main/examples/fee-payer-and-webauthn) | Sponsor transactions via domain-bound Passkeys. | [Guide](/docs/guides/fee-sponsorship) | ## Secure Origins (HTTPS) The Tempo Accounts SDK is designed to be used on secure origins (HTTPS). If you are using HTTP, it will fallback to using a popup instead of an iframe. This is because WebAuthn is not supported on iframes embedded on insecure origins (HTTP). Web frameworks typically default to HTTP in development environments. You will need to ensure to turn on HTTPS in development to leverage the iframe dialog. ### Portless [Portless](https://github.com/vercel-labs/portless) replaces port numbers with stable, named `.localhost` URLs and can enable HTTPS with auto-generated certificates. ```sh npm install -g portless portless run dev # → https://myapp.localhost ``` ### Next.js HTTPS can be enabled on Next.js' dev server by setting the `--experimental-https` flag on the `next dev` command. ```bash next dev --experimental-https ``` ### Vite HTTPS can be enabled on Vite's dev server by installing and configuring the `vite-plugin-mkcert` plugin. ```bash npm i vite-plugin-mkcert ``` :::code-group ```ts [vite.config.ts] import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import mkcert from 'vite-plugin-mkcert' export default defineConfig({ plugins: [ mkcert(), react(), ], }) ``` ::: ## Getting Help Have questions or building something cool with the Tempo Accounts SDK? Join the Telegram group to chat with the team and other devs: [@mpp\_devs](https://t.me/mpp_devs) # Deploying to Production Each adapter has different production considerations. Pick the adapter you ship with below to walk through what to set up and check off the production checklist. The [Tempo Wallet adapter](/docs/adapters/tempo-wallet) embeds an iframe-hosted wallet served from `wallet.tempo.xyz`. Production readiness depends on configuring trusted hosts, CSP, and HTTPS so the dialog renders reliably across browsers. ## Production Checklist The production hostname is registered in the trusted hosts list. The Content Security Policy allows wallet.tempo.xyz as a frame source. The app is served over HTTPS. The iframe dialog mounts visibly on every page that may trigger it. ## Trusted Hosts To enable the `iframe` dialog on all browsers in production, ensure your website hostname is added to the [trusted hosts list](https://github.com/tempoxyz/accounts/blob/main/src/trusted-hosts.json). Without this, the `iframe` dialog will fall back to a popup on browsers that do not support the [IntersectionObserver v2 API](https://web.dev/articles/intersectionobserver-v2). ## Content Security Policy If you've deployed a Content Security Policy, ensure that you have the Tempo Accounts SDK configured with your CSPs. The full set of directives that the Tempo Accounts SDK requires are: | Directive | Value | | ----------- | -------------------------- | | `frame-src` | `https://wallet.tempo.xyz` | ## HTTPS Browsers refuse to load secure third-party iframes from insecure origins. Tempo Wallet's iframe communication and passkey ceremonies both require a secure context, so production traffic must be served over HTTPS. ## Iframe Visibility [IntersectionObserver v2](https://web.dev/articles/intersectionobserver-v2) disables iframes that are obscured by overlays, modals, or sticky headers. When the dialog is triggered, verify it is not covered by any other UI — otherwise the user will see a blank surface. The [WebAuthn adapter](/docs/adapters/webauthn) binds account keys to your own domain. Treat passkey infrastructure with the same care as user authentication. ## Production Checklist The WebAuthn origin is served over HTTPS in production. The WebAuthn handler is backed by persistent [Kv](/docs/server/kv) storage. The RP ID fits both the current hostname and any future subdomain plan. Users can register a backup device or hardware security key. Specific errors are surfaced for failed ceremonies and origin mismatches. Registration and login ceremony endpoints are rate-limited. ## HTTPS Origin WebAuthn requires a [secure context](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts) outside of `localhost`. Production passkey ceremonies will fail outright over HTTP. ## Persistent Storage Memory-backed [`Kv`](/docs/server/kv) loses challenges and credential records on restart. Back `Handler.webAuthn` with `Kv.cookie`, `Kv.durableObject`, or `Kv.cloudflare` so registrations survive deploys and restarts. ## Choosing an `rpId` Passkeys are bound to the `rpId` at registration. Changing it later invalidates every existing credential and locks users out — pick a value that covers any subdomain you may add. ## Multiple Credentials per User Passkeys live on individual authenticators. Letting users add a backup device or hardware security key prevents lockouts when one device is lost or wiped. ## Surfacing Clear Errors WebAuthn ceremony failures are easy to misdiagnose. Surface specific error messages for expired challenges, canceled prompts, and RP ID or origin mismatches so users (and support) can recover quickly. ## Rate Limiting Unbounded `/register/options`, `/register`, `/login/options`, and `/login` endpoints are an enumeration and brute-force vector. Apply per-IP and per-user rate limits. The [Private Key adapter](/docs/adapters/private-key) signs in-process with a `secp256k1` private key. Production use is appropriate for server-side signers, fee payers, and scripts. ## Production Checklist Production keys are held only in trusted server environments. One key is used per role and per environment. Private keys, derived mnemonics, and raw secret values are never logged. A key rotation runbook is in place before shipping. ## Trusted Server Environments Browsers, CI logs, and developer laptops are not trusted environments. Production keys belong on the same machine that signs with them, behind your secret manager. ## One Key per Role and Environment Sharing a key across dev, staging, and production means one leak compromises every environment. Separate keys also make audit logs and on-chain attribution meaningful. ## Never Log Secrets Logs flow into observability tools, error trackers, and backups. Strip private keys, derived mnemonics, and raw secret values at the source — never emit them, even at `debug` level. ## Key Rotation Runbook When a key leaks, you need a tested process to mint, deploy, and revoke before an attacker acts. Practice the runbook before you need it. # Frequently Asked Questions ## What adapter should I use? Most apps should use [Tempo Wallet](/docs/adapters/tempo-wallet) - it provides a universal account experience with a central account for users that includes embedded onramp, access keys, and transaction orchestration out of the box. Use [domain-bound passkeys](/docs/adapters/webauthn) when you want to build your own wallet experience or when your app needs to manage and own the WebAuthn ceremony directly. [Learn more](/docs/guides/connect-accounts). ## Which browsers are supported? The Tempo Accounts SDK supports the following browsers: * Safari * Chrome * Firefox * Brave * Arc If a browser is not listed above, it likely works but is not officially supported. ## Which password managers are supported? The Tempo Accounts SDK supports the following password managers: * iCloud Keychain * Google Password Manager * 1Password * Bitwarden If a password manager is not listed above, it likely works but is not officially supported. ## Which operating systems are supported? The Tempo Accounts SDK supports the following operating systems: * iOS * iPadOS * macOS * Android * Linux * Windows If an operating system is not listed above, it likely works but is not officially supported. ## How does the Tempo Accounts SDK work with my Content Security Policy? If you've deployed a Content Security Policy, see the [Deploying to Production](/docs/production) page for the full set of required directives. # Connect Accounts Connect a Tempo account in your application using the [Tempo Accounts SDK](/docs) and [Wagmi](https://wagmi.sh). The Tempo Accounts SDK ships several signing adapters — [Tempo Wallet](/docs/adapters/tempo-wallet), [domain-bound Passkeys](/docs/adapters/webauthn), [Turnkey](/docs/adapters/turnkey), [Privy](/docs/adapters/privy), and more. Each adapter exposes the same Wagmi connector surface, so the integration code in this guide is the same regardless of which adapter you pick. Choose an adapter once in your Wagmi config; the rest of your app stays adapter-agnostic. :::info\[Which adapter should I use?] Most apps should start with **Tempo Wallet** for a universal, hosted account experience. Pick **domain-bound Passkeys** if you want full control over the auth ceremony on your own origin, or another adapter if you have an existing custody stack. See the [Adapters overview](/docs/adapters) for a side-by-side comparison. ::: ## Demo By the end of this guide, you will be able to let users connect a Tempo account from your application using your adapter of choice. } className="flex justify-start"> ## Walkthrough ::::steps ### Install the SDK Install `accounts` together with Wagmi and Viem. :::code-group ```bash [npm] npm i accounts wagmi viem ``` ```bash [pnpm] pnpm i accounts wagmi viem ``` ```bash [bun] bun i accounts wagmi viem ``` ::: ### Set up Wagmi Ensure that you have set up your project with Wagmi by following the [official guide](https://wagmi.sh/react/getting-started). ### Configure an Adapter Next, register a Tempo signing adapter as a Wagmi connector. Pick the adapter that fits how you want to custody accounts — the rest of the app code stays the same. :::code-group ```tsx twoslash [Tempo Wallet] // @noErrors import { createConfig, http } from 'wagmi' import { tempo } from 'wagmi/chains' import { tempoWallet } from 'wagmi/connectors' // [!code ++] export const config = createConfig({ chains: [tempo], connectors: [tempoWallet()], // [!code ++] multiInjectedProviderDiscovery: false, transports: { [tempo.id]: http(), }, }) ``` ```tsx twoslash [Domain-bound Passkeys] // @noErrors import { createConfig, http } from 'wagmi' import { tempo } from 'wagmi/chains' import { webAuthn } from 'wagmi/tempo' // [!code ++] export const config = createConfig({ chains: [tempo], connectors: [webAuthn({ authUrl: '/auth' })], // [!code ++] multiInjectedProviderDiscovery: false, transports: { [tempo.id]: http(), }, }) ``` ::: :::tip This Wagmi configuration sets `multiInjectedProviderDiscovery` to `false` to prevent injected browser wallets from being detected, and to prefer the Tempo connector. If you would like to allow connection to other wallets, set this property to `true`. ::: ### Display Sign In Button After that, set up a "Sign in" button that triggers the connector for the user. We will create a new `Example.tsx` component to work in. :::code-group ```tsx twoslash [Example.tsx] // @noErrors import { useConnect, useConnectors } from 'wagmi' export function Example() { const connect = useConnect() const [connector] = useConnectors() return (
) } ``` ::: ### Display Account & Sign Out After the user has signed in, display the account information and a sign out button. ```tsx twoslash [Example.tsx] // @noErrors import { useConnection, useConnect, useConnectors, useDisconnect } from 'wagmi' export function Example() { const account = useConnection() // [!code ++] const connect = useConnect() const [connector] = useConnectors() const disconnect = useDisconnect() // [!code ++] if (account.address) // [!code ++] return ( // [!code ++]
{/* [!code ++] */}
{account.address.slice(0, 6)}...{account.address.slice(-4)}
{/* [!code ++] */} {/* [!code ++] */}
{/* [!code ++] */} ) // [!code ++] return (
) } ``` ### Deploy to Production When you're ready to go live, follow the [Deploying to Production](/docs/production) guide to configure your application for production use. :::: ## Recipes ### Request a Verified Email Pass the `identity` capability to request the account's verified email during connect. Set `withCapabilities: true` so the connect result includes per-account `capabilities`, then read the email from the authenticated account. ```tsx twoslash [Example.tsx] // @noErrors import { useConnect, useConnectors } from 'wagmi' export function Example() { const connect = useConnect() const [connector] = useConnectors() return ( ) } ``` :::warn The plain `email` string is fine for client-side display, but anyone can set it. The result also carries a signed `idToken` that your server can verify to trust the email. See [Verify the Email Server-Side](/docs/guides/authentication#verify-the-email-server-side). ::: ## Best Practices ### Deposit Funds If users should have a chance to fund their account immediately after connecting, pass the `showDeposit` capability. The wallet shows the standard deposit picker after connect succeeds. The prompt is optional, so the user can continue without depositing. ```tsx twoslash [Example.tsx] // @noErrors import { useConnect, useConnectors } from 'wagmi' export function Example() { const connect = useConnect() const [connector] = useConnectors() return ( ) } ``` :::tip Use the object form when you want to pre-fill deposit details. ```tsx twoslash [Example.tsx] // @noErrors connect.connect({ connector, capabilities: { // [!code focus] showDeposit: { // [!code focus] amount: '10', // [!code focus] token: 'USDC.e', // [!code focus] }, // [!code focus] }, // [!code focus] }) ``` ::: ### Loading State When the user is signing in or signing out, show loading state to indicate that the process is happening. Use the `isPending` property from the `useConnect` hook to show pending state to the user. ```tsx twoslash [Example.tsx] // @noErrors import { useConnection, useConnect, useConnectors, useDisconnect } from 'wagmi' export function Example() { const account = useConnection() const connect = useConnect() const [connector] = useConnectors() const disconnect = useDisconnect() if (connect.isPending) // [!code ++] return
Check prompt...
{/* [!code ++] */} return (/* ... */) } ``` ### Error Handling If an error unexpectedly occurs, display an error message to the user. Use the `error` property from the `useConnect` hook to show error state to the user. ```tsx twoslash [Example.tsx] // @noErrors import { useConnection, useConnect, useConnectors, useDisconnect } from 'wagmi' export function Example() { const account = useConnection() const connect = useConnect() const [connector] = useConnectors() const disconnect = useDisconnect() if (connect.error) // [!code ++] return
Error: {connect.error.message}
{/* [!code ++] */} return (/* ... */) } ``` ## Learning Resources # Authentication Issue a server-side session for a connected Tempo account. The wallet signs a [SIWE](https://eips.ethereum.org/EIPS/eip-4361) challenge during `wallet_connect`, and your server verifies it to authenticate follow-up requests. The cookie is issued by **your origin**. The SDK calls your `verify` endpoint from the app so the browser attaches the `Set-Cookie` to your domain. ## Demo By the end of this guide, you will be able to authenticate the connected account against your own server and read the session from any request. Hit **GET /me** before signing in to confirm the endpoint returns `401`. Sign in, then fetch again to see the authenticated response. }> ## Walkthrough ::::steps ### Install the SDK Install `accounts` together with Wagmi, Viem, and a server framework. This guide uses [Hono](https://hono.dev) on Cloudflare Workers; the same handler shape works on Node.js, Bun, Deno, Express, and Next.js. :::code-group ```bash [npm] npm i accounts wagmi viem hono ``` ```bash [pnpm] pnpm i accounts wagmi viem hono ``` ```bash [bun] bun i accounts wagmi viem hono ``` ::: ### Set up the Auth Handler Create a [`Handler.auth`](/docs/server/handler.auth) instance on your server. It mounts three endpoints under `path`: * `POST /auth/challenge` — issue a single-use SIWE challenge * `POST /auth` — verify a signed challenge and issue a session * `POST /auth/logout` — revoke a session It also exposes a `getSession` helper for reading the session from other routes. ```ts twoslash [worker/index.ts] // @noErrors import { Handler, Kv } from 'accounts/server' import { env } from 'cloudflare:workers' // `Kv.durableObject(env.NONCE_DO)` gives the auth handler a linearizable // store for one-time-consume SIWE challenge nonces and issued sessions // across concurrent worker instances. export const auth = Handler.auth({ path: '/auth', store: Kv.durableObject(env.NONCE_DO), }) ``` :::tip\[Pick the right store for production] The default `Kv.memory()` store is fine for development. Multi-instance deployments need a linearizable backend so a SIWE nonce can only be consumed once. On Cloudflare, use [`Kv.durableObject`](/docs/server/kv.durableObject). Elsewhere, any [`Kv`](/docs/server/kv) implementation that supports atomic `take` works. ::: ### Mount the Handler Wire the handler into your server, and expose any routes that should be authenticated by reading the session via `getSession`. ```ts twoslash [worker/index.ts] // @noErrors import { Handler, Kv } from 'accounts/server' import { env } from 'cloudflare:workers' import { Hono } from 'hono' // Re-export the reference Durable Object class so wrangler can wire it up. export const NonceStorage = Kv.NonceStorage const auth = Handler.auth({ path: '/auth', store: Kv.durableObject(env.NONCE_DO), }) const app = new Hono() // Forward every request under `/auth` to the auth handler so the SDK // [!code ++] // can hit `/auth/challenge`, `/auth`, and `/auth/logout`. // [!code ++] app.all('/auth', (c) => auth.fetch(c.req.raw)) // [!code ++] app.all('/auth/*', (c) => auth.fetch(c.req.raw)) // [!code ++] // Reads the SIWE-issued session and returns the connected address. // [!code ++] // Demonstrates how an authenticated endpoint consumes Handler.auth. // [!code ++] app.get('/me', async (c) => { // [!code ++] const session = await auth.getSession(c.req.raw) // [!code ++] if (!session) return c.json({ error: 'unauthenticated' }, 401) // [!code ++] return c.json({ address: session.address, chainId: session.chainId }) // [!code ++] }) // [!code ++] export default app ``` :::tip\[Using a different framework?] `Handler.auth` exposes both `fetch` (Web Fetch API) and `listener` (Node.js `RequestListener`) entrypoints, so you can plug it into any server framework: ```ts createServer(auth.listener) // Node.js Bun.serve({ fetch: auth.fetch }) // Bun Deno.serve({ fetch: auth.fetch }) // Deno app.all('*', c => auth.fetch(c.request)) // Elysia app.use(auth.listener) // Express app.use(c => auth.fetch(c.req.raw)) // Hono export const GET = auth.fetch // Next.js export const POST = auth.fetch // Next.js ``` ::: ### Configure the Connector Pass the auth endpoint to the connector via the `auth` capability. The SDK runs the SIWE round-trip inside `wallet_connect`, sharing the same passkey ceremony. ```tsx twoslash [src/config.ts] // @noErrors import { createConfig, http } from 'wagmi' import { tempo, tempoModerato } from 'wagmi/chains' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ chains: [tempoModerato, tempo], connectors: [tempoWallet({ auth: '/auth' })], // [!code ++] multiInjectedProviderDiscovery: false, transports: { [tempo.id]: http(), [tempoModerato.id]: http(), }, }) ``` :::tip\[Object form] Use the [object form](/docs/rpc/wallet_connect) to override individual endpoints, opt into a `{ token }` body via `returnToken`, or split challenge/verify across hosts. ```ts tempoWallet({ auth: { challenge: 'https://auth.example.com/challenge', verify: 'https://auth.example.com/verify', logout: 'https://auth.example.com/logout', returnToken: true, }, }) ``` ::: ### Display the Sign-In Button Drop in a connect button — no auth-specific code required. The Wagmi `connect` mutation triggers `wallet_connect`, and the auth round-trip happens inside that call. ```tsx twoslash [src/App.tsx] // @noErrors import { useConnect, useConnection, useConnectors, useDisconnect } from 'wagmi' function Connect() { const { address } = useConnection() const { mutate: connect, isPending } = useConnect() const { mutate: disconnect } = useDisconnect() const [connector] = useConnectors() if (address) return ( ) return ( ) } ``` ### Call authenticated resource With the session cookie attached to your origin, any authenticated route just works. Use `credentials: 'include'` so the cookie is sent on the fetch. ```tsx twoslash [src/App.tsx] // @noErrors import { useState } from 'react' function Me() { const [me, setMe] = useState(null) async function fetchMe() { const res = await fetch('/me', { credentials: 'include' }) // [!code focus] setMe(await res.json()) } return (
{JSON.stringify(me, null, 2)}
) } ``` ### Sign Out You don't need to call `/auth/logout` yourself: `wallet_disconnect` automatically POSTs to the configured `logout` endpoint, which clears the session cookie. The next `GET /me` returns `401`. ```tsx twoslash // @noErrors import { useDisconnect } from 'wagmi' const { mutate: disconnect } = useDisconnect() // Clears the wallet connection AND the server session cookie. disconnect() ``` ### Deploy to Production When you're ready to go live, follow the [Deploying to Production](/docs/production) guide. Then pin a canonical [`origin`](/docs/server/handler.auth#origin) on `Handler.auth` to prevent a spoofed `x-forwarded-host` from shifting the SIWE domain. ```ts twoslash // @noErrors import { Handler, Kv } from 'accounts/server' const auth = Handler.auth({ origin: 'https://app.example.com', // [!code ++] path: '/auth', store: Kv.durableObject(env.NONCE_DO), }) ``` :::: ## Recipes ### Request a Verified Email Alongside the authentication session, you can request the account's verified email via the `identity` capability. Set `withCapabilities: true` so the connect result includes per-account `capabilities`, then read the email from the authenticated account. ```tsx twoslash [src/App.tsx] // @noErrors import { useConnect, useConnectors } from 'wagmi' function Connect() { const { connectAsync } = useConnect() const [connector] = useConnectors() return ( ) } ``` :::warn The plain `email` string is fine for client-side display, but anyone can set it. The result also carries a signed `idToken` that your server can verify to trust the email. See [Verify the Email Server-Side](#verify-the-email-server-side). ::: #### Verify the Email Server-Side Alongside the plain `email`, the wallet returns a signed token proving the email belongs to the account. Your auth handler checks the signature and adds the verified email to the session automatically — set [`identity`](/docs/server/handler.auth#identity) only to require an email or use your own wallet. ```ts twoslash [worker/index.ts] // @noErrors import { Handler, Kv } from 'accounts/server' import { env } from 'cloudflare:workers' const auth = Handler.auth({ path: '/auth', store: Kv.durableObject(env.NONCE_DO), }) // In an authenticated route, the verified email is on the session. // [!code focus] app.get('/me', async (c) => { // [!code focus] const session = await auth.getSession(c.req.raw) // [!code focus] if (!session) return c.json({ error: 'unauthenticated' }, 401) return c.json({ address: session.address, email: session.email }) // [!code focus] }) ``` ## Notes & Gotchas ### Cookies are issued by your origin The session cookie is set on **your app's** origin, not the wallet's. The SDK sees `Set-Cookie` because it makes the verify call itself. This means `/auth` and `/me` must live on the same origin as your app for the cookie to attach. ### Same-origin in development Browsers attach `SameSite=Lax` cookies only on same-site requests. Run your auth handler on the app's origin — Vite's proxy or a colocated worker (see [`authentication`](https://github.com/tempoxyz/accounts/tree/main/examples/authentication)) is the easiest path. ### Non-browser clients Use the object form with `returnToken: true` when the SDK can't store cookies (CLI, React Native). The verify response then returns `{ token }` for use as `Authorization: Bearer `. ```ts tempoWallet({ auth: { url: '/auth', returnToken: true } }) ``` ### Custom verification logic Pass [`onAuthenticate`](/docs/server/handler.auth#onauthenticate) to gate session issuance. Throw to reject the request with a `401`, or return a `Response` to merge custom JSON onto the verify reply. ## Next Steps # Identity The Accounts SDK lets an app request **identity claims** about a connected account and verify them on its own server. Claims are issued by the wallet through an [OpenID Connect](https://openid.net/developers/how-connect-works/) provider: the wallet signs each claim into a standard ID token (a JWT) that your backend checks against the wallet's public keys, so you can trust the claim without trusting your frontend. When you request a verified email during [`wallet_connect`](/docs/rpc/wallet_connect#requesting-a-verified-email), the wallet returns two things on the connected account: * `capabilities.identity.email`: a plain string, handy for display. * `capabilities.identity.idToken`: a signed identity token (a JWT) that **proves** the email belongs to the account. The plain string is convenient for client-side display, but anyone can put any value there. To trust the email on your server, verify the token. ## How It Works The token is a standard [OpenID Connect](https://openid.net/developers/how-connect-works/) ID token signed by the wallet's keys. It is bound to: * your app's origin (the `aud` claim), so a token issued for another app cannot be replayed against yours, and * the connected account (the `sub` claim), so it cannot be reused for a different address. The wallet runs a verify-only identity issuer. Anyone can fetch its public signing keys to check a token's signature: * Issuer: `https://wallet.tempo.xyz/api/oidc` * Discovery: `https://wallet.tempo.xyz/api/oidc/.well-known/openid-configuration` * JWKS: `https://wallet.tempo.xyz/api/oidc/.well-known/jwks.json` ## Automatic Verification If you authenticate accounts with [`built-in Authentication`](/docs/guides/authentication) with the `identity` capability, verification is automatic. ::::steps ### Connect Request the `identity` capability in the Wagmi `connect` call. Your connector is already wired to your auth endpoint (see [Authentication](/docs/guides/authentication#configure-the-connector)), so the SDK forwards the signed token to your server during the connect ceremony. ```tsx twoslash [src/App.tsx] // @noErrors import { useConnect, useConnectors } from 'wagmi' function Connect() { const { connectAsync } = useConnect() const [connector] = useConnectors() return ( ) } ``` :::info\[Without Wagmi] Using the [`Provider`](/docs/api/provider) directly, request the capability on [`wallet_connect`](/docs/rpc/wallet_connect#requesting-a-verified-email): ```ts twoslash // @noErrors import { Provider } from 'accounts' const provider = Provider.create() const { accounts } = await provider.request({ method: 'wallet_connect', params: [{ capabilities: { identity: { email: true } } }], // [!code focus] }) ``` ::: ### Read Email from Session The verified email is on the session. Read it from any authenticated route with `getSession`. ```ts twoslash [worker/index.ts] // @noErrors import { Handler, Kv } from 'accounts/server' import { env } from 'cloudflare:workers' const auth = Handler.auth({ path: '/auth', store: Kv.durableObject(env.NONCE_DO), }) app.get('/me', async (c) => { const session = await auth.getSession(c.req.raw) // [!code focus] if (!session) return c.json({ error: 'unauthenticated' }, 401) return c.json({ address: session.address, email: session.email }) // [!code focus] }) ``` :::: ## Manual Verification If you mint your own session (for example in [`onAuthenticate`](/docs/server/handler.auth#onauthenticate) with `session: false`) or verify the token in a separate service, read the token on the client and verify it yourself with [`Identity.verify`](/docs/server/identity.verify). ::::steps ### Read Token and Send to Server Request the `identity` capability, read the signed `idToken` off the connected account, and POST it to your own endpoint. ```tsx twoslash [src/App.tsx] // @noErrors import { useConnect, useConnectors } from 'wagmi' function Connect() { const { connectAsync } = useConnect() const [connector] = useConnectors() return ( ) } ``` :::info\[Without Wagmi] Using the [`Provider`](/docs/api/provider) directly, read the signed `idToken` off the connected account: ```ts twoslash // @noErrors import { Provider } from 'accounts' const provider = Provider.create() const { accounts } = await provider.request({ method: 'wallet_connect', params: [{ capabilities: { identity: { email: true } } }], }) const idToken = accounts[0]?.capabilities?.identity?.idToken // [!code focus] ``` ::: ### Verify Token on Server Verify the token with `Identity.verify`. It fetches the issuer's JWKS, then asserts the signature, `iss`, `aud`, `exp`, and `email_verified === true`. Pass `subject` and/or `nonce` to also cross-check the token's `sub` and `nonce`. It throws on any failure. ```ts twoslash [worker/index.ts] // @noErrors import { Identity } from 'accounts/server' app.post('/verify-email', async (c) => { const { idToken } = await c.req.json() const claims = await Identity.verify(idToken, { // [!code focus] audience: 'https://app.example.com', // your app origin (token `aud`) // [!code focus] issuer: 'https://wallet.tempo.xyz/api/oidc', // the wallet's identity issuer // [!code focus] }) // [!code focus] return c.json({ email: claims.email }) // verified email // [!code focus] }) ``` :::: ## Require a Specific Email Pass an email address instead of `true` to require that exact email. The wallet pre-fills and locks the sign-in entry to it: the user must authenticate with the account that owns the email (verifying it with a one-time code first when needed), and connecting an account whose verified email differs is rejected. Use this when your app already knows who is signing in — an invite link, a pre-auth session, or a server-side allowlist. ```tsx twoslash [src/App.tsx] // @noErrors import { useConnect, useConnectors } from 'wagmi' function Connect() { const { connectAsync } = useConnect() const [connector] = useConnectors() return ( ) } ``` To also bind an explicit nonce into the issued token, use the object form: `identity: { email: { address: 'user@example.com', nonce } }` (see [`wallet_connect`](/docs/rpc/wallet_connect#requiring-a-specific-email)). ## Restrict Email Domains Pass `{ domains }` to restrict email entry to an allowlist of domains. The wallet rejects entering an email outside them, so users whose sign-ups your server would refuse get immediate feedback instead of a rejection after authenticating. ```tsx twoslash [src/App.tsx] // @noErrors import { useConnect, useConnectors } from 'wagmi' function Connect() { const { connectAsync } = useConnect() const [connector] = useConnectors() return ( ) } ``` The restriction applies to email entry only: an account whose email is already verified still shares it, since your server may accept existing or invited users beyond the allowlist. Always enforce the policy server-side by verifying the `idToken` (see [`wallet_connect`](/docs/rpc/wallet_connect#restricting-email-domains)). ## Verify From Any Stack The token is a standard OIDC ID token, so any JWT/OIDC library can verify it against the JWKS above. You do not need the Accounts SDK. For example, with [`jose`](https://github.com/panva/jose): ```ts twoslash // @noErrors import { createRemoteJWKSet, jwtVerify } from 'jose' const jwks = createRemoteJWKSet( new URL('https://wallet.tempo.xyz/api/oidc/.well-known/jwks.json'), ) const { payload } = await jwtVerify(idToken, jwks, { issuer: 'https://wallet.tempo.xyz/api/oidc', audience: 'https://app.example.com', }) if (payload.email_verified !== true) throw new Error('email not verified') payload.email // verified email ``` Always check `email_verified === true`, and cross-check `sub` against the address you expect. ## Custom Wallets If you run your own wallet, point verification at its identity issuer instead: * `Handler.auth({ identity: { issuer: 'https://wallet.example.com/api/oidc' } })`, or * `Identity.verify(idToken, { issuer: 'https://wallet.example.com/api/oidc', ... })`. # Transfers Send stablecoin transfers from a connected Tempo account using the [Tempo Accounts SDK](/docs). There are two common ways to move value out of a connected account: * **User-initiated transfers**: your UI calls `wallet_transfer` and the connected account signs and broadcasts a stablecoin transfer. * **Server-initiated transfers**: your server responds with HTTP `402 Payment Required` using the [Machine Payments Protocol (MPP)](https://mpp.dev) [charge method](https://mpp.dev/payment-methods/tempo/charge), describing the amount, currency, and recipient. The Tempo Accounts SDK auto-fulfills the challenge from the connected account and retries the request, so your client code stays a plain `fetch`. ## Demo By the end of this section you'll have a button that sends pathUSD from the connected account to a recipient. } className="flex flex-col gap-3"> } /> ## Walkthrough ::::steps ### Connect an Account Follow the [Connect Accounts](/docs/guides/connect-accounts) guide to set up Wagmi with a Tempo signing adapter and let the user sign in. This guide will still function regardless of adapter. ### Send the Transfer Call [`Hooks.wallet.useTransfer`](https://wagmi.sh/tempo/hooks/wallet.useTransfer) from `wagmi/tempo` and pass the amount, recipient, and token. ```tsx twoslash [Transfer.tsx] // @noErrors import { Hooks } from 'wagmi/tempo' export function Transfer() { const transfer = Hooks.wallet.useTransfer() return ( ) } ``` :::: ## Best Practices ### Loading State Use `transfer.isPending` to disable the button and show a pending label while the transfer is in flight. ```tsx twoslash [Transfer.tsx] // @noErrors import { Hooks } from 'wagmi/tempo' export function Transfer() { const transfer = Hooks.wallet.useTransfer() return ( ) } ``` ### Error Handling If the transfer fails — for example, the user rejects the request, or the access key has insufficient allowance — surface the error from `transfer.error` to the user. ```tsx twoslash [Transfer.tsx] // @noErrors import { Hooks } from 'wagmi/tempo' export function Transfer() { const transfer = Hooks.wallet.useTransfer() return (
{transfer.error && ( // [!code ++]
{`${transfer.error.name}: ${transfer.error.message}`}
/* [!code ++] */ )} {/* [!code ++] */}
) } ``` ### Success State When the transfer succeeds, `transfer.data.receipt.transactionHash` is the on-chain transaction hash. Link to the block explorer, refresh balances, or surface a success state to the user. ```tsx twoslash [Transfer.tsx] // @noErrors import { tempoModerato } from 'wagmi/chains' // [!code ++] import { Hooks } from 'wagmi/tempo' export function Transfer() { const transfer = Hooks.wallet.useTransfer() return (
{transfer.isSuccess && ( // [!code ++] {/* [!code ++] */} See receipt {/* [!code ++] */} /* [!code ++] */ )} {/* [!code ++] */}
) } ```
## Demo By the end of this section you'll have an HTTP endpoint that charges $100 in pathUSD per call before returning its payload. The button below hits a real `/api/transfer` endpoint hosted on this docs site — the connected account auto-fulfills the `402 Payment Required` challenge and the request resolves only after settlement. } className="flex flex-col gap-3"> } /> ## Walkthrough ::::steps ### Install the Server Dependencies Install [`mppx`](https://github.com/tempoxyz/mppx) for serving and verifying HTTP `402` payment challenges. This guide uses [Hono](https://hono.dev) on Cloudflare Workers; the same handler shape works on Node.js, Bun, Deno, Express, and Next.js. :::code-group ```bash [npm] npm i mppx hono ``` ```bash [pnpm] pnpm i mppx hono ``` ```bash [bun] bun i mppx hono ``` ::: ### Define a Charge Gate the endpoint behind a stablecoin transfer. If the client hasn't paid, return the payment challenge. Once paid, return your payload with a verifying receipt. ```ts twoslash [worker/index.ts] // @noErrors import { Hono } from 'hono' import { Mppx, tempo } from 'mppx/server' const pathUsd = '0x20c0000000000000000000000000000000000000' const mppx = Mppx.create({ methods: [tempo.charge({ testnet: true })], secretKey: process.env.MPP_SECRET_KEY, }) const app = new Hono() app.get('/api/transfer', async (c) => { const result = await mppx.charge({ amount: '100', currency: pathUsd, recipient: '0x0000000000000000000000000000000000000001', })(c.req.raw) if (result.status === 402) return result.challenge return result.withReceipt( Response.json({ message: 'Thanks for your $100.', }), ) }) export default app ``` ### Call It from the Client No extra client code is required. Use `fetch` (or any HTTP client) the same way you would for any other endpoint. The Tempo Accounts SDK transparently handles the `402`, signs a transfer with the connected account, and replays the request. ```tsx twoslash [Charge.tsx] // @noErrors import { useState } from 'react' export function Charge() { const [data, setData] = useState() const [isPending, setPending] = useState(false) return (
{data !== undefined &&
{JSON.stringify(data, null, 2)}
}
) } ``` :::: ## Best Practices ### React Query for Mutations Reach for `useMutation` from `@tanstack/react-query` instead of hand-rolling `useState` for `data`, `error`, and `isPending`. You get retries, cache invalidation, and consistent loading/error semantics for free, and the rest of your app can react to the latest charge result. ```tsx twoslash [Charge.tsx] // @noErrors import { useMutation } from '@tanstack/react-query' export function Charge() { const charge = useMutation({ mutationFn: async () => { const res = await fetch('/api/transfer') if (!res.ok) throw new Error(await res.text()) return res.json() }, }) return (
{charge.error &&
{charge.error.message}
} {charge.data &&
{JSON.stringify(charge.data, null, 2)}
}
) } ``` ### Identify the Payer After `mppx.charge` verifies payment, the `Authorization` header is still on the request. Decode it with `Credential.deserialize` to read the payer's DID (e.g. `did:pkh:eip155:1:0x...`). Use it for per-payer rate limiting, analytics, or audit logs. ```ts twoslash [worker/index.ts] // @noErrors import { Credential } from 'mppx' app.get('/api/transfer', async (c) => { const result = await mppx.charge({ /* ... */ })(c.req.raw) if (result.status === 402) return result.challenge const credential = Credential.deserialize(c.req.header('Authorization')!) // [!code ++] const payer = credential.source // "did:pkh:eip155:1:0x..." // [!code ++] return result.withReceipt(Response.json({ payer })) }) ``` ### Reuse Credentials with `Expires` By default, each request requires a fresh on-chain transfer. For tightly coupled flows (e.g. a paid endpoint that the user hits repeatedly within a short window), set `expires` so a paid credential is reusable for that duration. The user pays once and reuses the credential until it expires. ```ts twoslash [worker/index.ts] // @noErrors import { Expires } from 'mppx' const result = await mppx.charge({ amount: '100', currency: pathUsd, recipient: '0x0000000000000000000000000000000000000001', expires: Expires.minutes(5), // [!code ++] })(c.req.raw) ```
## Next Steps # Spend Permissions Spend permissions let a connected Tempo account approve a scoped access key once, then use that key for repeat activity that stays inside the approved policy. Use them when an app needs to make the same kind of payment more than once: checkouts, metered APIs, recurring actions, or any flow where asking for a confirmation prompt on every transfer would be noisy. The user still approves the boundary up front: token, amount, allowed call targets, optional recipients, and expiry. ## Demo By the end of this guide you'll configure a bounded spend permission: the user signs in, approves the policy once, then matching transfers can be signed locally without another Tempo Wallet prompt. } className="flex flex-col gap-3" wagmiConfig="spendPermissions"> } /> ## Walkthrough ::::steps ### Connect an Account Follow the [Connect Accounts](/docs/guides/connect-accounts) guide to set up Wagmi with a Tempo signing adapter and let the user sign in. This guide uses Tempo Wallet because the example is hosted there, but the `authorizeAccessKey` shape is the same for adapters that support access key authorization. ### Define the Spend Policy Add `authorizeAccessKey` to the connector. The object describes the permission the user will approve when a matching local key is not already available. Pass a function instead when the policy needs to be computed dynamically. ```tsx twoslash [src/config.ts] // @noErrors import { Expiry } from 'accounts' import { parseUnits } from 'viem' import { createConfig, http } from 'wagmi' import { tempo, tempoModerato } from 'wagmi/chains' import { tempoWallet } from 'wagmi/connectors' const pathUsd = '0x20c0000000000000000000000000000000000000' as const export const config = createConfig({ chains: [tempoModerato, tempo], connectors: [ tempoWallet({ testnet: true, authorizeAccessKey: { // [!code focus] expiry: Expiry.days(1), // [!code focus] limits: [ // [!code focus] { token: pathUsd, limit: parseUnits('100', 6) }, // [!code focus] ], // [!code focus] scopes: [ // [!code focus] { address: pathUsd, selector: 'transfer(address,uint256)' }, // [!code focus] ], // [!code focus] reuse: { // [!code focus] minExpiry: Expiry.hours(1), // [!code focus] minLimits: [ // [!code focus] { token: pathUsd, limit: parseUnits('10', 6) }, // [!code focus] ], // [!code focus] }, // [!code focus] }, // [!code focus] }), ], multiInjectedProviderDiscovery: false, transports: { [tempo.id]: http(), [tempoModerato.id]: http(), }, }) ``` :::warning\[Bound permissions are required] Tempo Wallet approval pages require at least one `limits` entry and one `scopes` entry. `limits` define how much the access key can spend, while `scopes` define which calls it can make. Without both, the approval would be too broad for a user to review safely. Local adapters may accept broader keys, but production apps should keep the same bounded policy everywhere. ::: ### Sign In with the Policy Use the same sign-in button from the Connect Accounts guide. If the SDK already has a stored access key that matches the scopes and optional `reuse` policy, sign-in continues without a new access key prompt. Otherwise Tempo Wallet signs the access key authorization returned by `authorizeAccessKey`. ```tsx twoslash [src/Connect.tsx] // @noErrors import { useConnect, useConnectors } from 'wagmi' export function Connect() { const connect = useConnect() const [connector] = useConnectors() return ( ) } ``` ### Send Within the Permission Submit calls that match the approved scopes. The SDK selects the access key when the transfer fits the spend policy; otherwise it falls back to the connected account's normal signing path. If no matching key exists at send time, the SDK checks whether the configured `authorizeAccessKey` scopes would cover the transaction. When they do, it requests the access key first, stores it, then sends the transaction with that key. When they do not, it skips the access key request and uses the normal signing path. ```tsx twoslash [src/Transfer.tsx] // @noErrors import { Hooks } from 'wagmi/tempo' export function Transfer() { const transfer = Hooks.wallet.useTransfer() return ( ) } ``` :::: ## Best Practices ### Pair Limits with Scopes Use `limits` for how much the key may spend, and `scopes` for what calls it may make. A transfer permission should usually include `transfer(address, uint256)` for the token you intend to support. ```tsx twoslash [src/config.ts] // @noErrors import { Expiry } from 'accounts' import { parseUnits } from 'viem' const pathUsd = '0x20c0000000000000000000000000000000000000' as const const spendPermission = { expiry: Expiry.days(1), limits: [{ token: pathUsd, limit: parseUnits('100', 6) }], scopes: [ { address: pathUsd, selector: 'transfer(address,uint256)' }, ], reuse: { minExpiry: Expiry.hours(1), minLimits: [{ token: pathUsd, limit: parseUnits('10', 6) }], }, } ``` The top-level `expiry`, `limits`, and `scopes` define the new key to create when needed. `reuse.minExpiry` and `reuse.minLimits` only decide whether an existing stored key is acceptable to reuse. ### Keep Expiry Short Start with a short expiry and ask the user to renew when the workflow needs more time. The access key example uses `Expiry.days(1)`, which is a good default for checkout and metered usage flows. ```ts twoslash import { Expiry } from 'accounts' const expiry = Expiry.days(1) ``` ### Use Recipient Scopes If your app always pays one recipient, include `recipients` on the scope. Only omit it when the recipient is truly user-selected at transfer time. ```ts twoslash // @noErrors const pathUsd = '0x20c0000000000000000000000000000000000000' as const const scopes = [ { address: pathUsd, selector: 'transfer(address,uint256)', recipients: ['0x0000000000000000000000000000000000000001'], }, ] ``` ### Match Token Decimals Build limits with `parseUnits` and the token's real decimal count. pathUSD uses 6 decimals, so `parseUnits('100', 6)` means a 100 pathUSD cap. ```ts twoslash import { parseUnits } from 'viem' const limit = parseUnits('100', 6) ``` ## Next Steps # React Native React Native apps connect to a wallet through [mobile web auth](/docs/api/mobileWebAuth): the provider opens the wallet in a system browser session, then receives an app-link callback with the encrypted response. This guide covers the platform setup. For choosing and configuring the adapter itself, see the [Tempo Wallet adapter](/docs/adapters/tempo-wallet) or the [mobileWebAuth reference](/docs/api/mobileWebAuth). ## Install ```sh npx expo install expo-web-browser expo-secure-store react-native-get-random-values react-native-mmkv react-native-nitro-modules ``` ## Connect ```ts twoslash import { Provider } from 'accounts' import { tempoWallet } from 'accounts/mobileWebAuth' import { openAuthSession } from 'accounts/react-native/expo-web-browser' import { secureMmkv } from 'accounts/react-native/secure-mmkv' const provider = Provider.create({ adapter: tempoWallet({ baseUrl: 'https://app.example', openAuthSession, redirectUri: 'com.example.app:/auth', }), storage: secureMmkv(), }) ``` ## Development Builds Use a development build with a reverse-DNS private-use callback URI (e.g. `com.example.app:/auth`). Expo Go-style callback URLs are not valid mobile web auth callbacks. The exact callback URI must be published in your app's [consumer discovery document](/docs/api/mobileWebAuth#consumer-discovery). ```sh expo run:ios ``` ## Polyfills Import polyfills before the SDK so module-scope code finds them: ```js // index.js import './polyfills' ``` ```js // polyfills.js import 'react-native-get-random-values' if (typeof globalThis.MessageEvent === 'undefined') { globalThis.MessageEvent = class MessageEvent { constructor(type, options = {}) { this.data = options.data ?? null this.origin = options.origin ?? '' this.type = type } } } ``` `react-native-get-random-values` provides `crypto.getRandomValues` for key generation. Hermes does not provide `MessageEvent`, which the SDK references at module scope. ## Browser Sessions `accounts/react-native/expo-web-browser` opens auth sessions with Expo WebBrowser. It is an optional subpath so apps that use another browser primitive do not install Expo WebBrowser — pass your own `openAuthSession` that opens the authorization URL and resolves with the callback URL instead. ```ts twoslash import { openAuthSession } from 'accounts/react-native/expo-web-browser' ``` ## Storage Provider state persists through a storage adapter. React Native adapters ship as optional subpaths: ```ts twoslash import { asyncStorage } from 'accounts/react-native/async-storage' import { secureMmkv } from 'accounts/react-native/secure-mmkv' ``` Use [`secureMmkv`](/docs/api/storage.secureMmkv) when the provider manages access keys. It stores the larger SDK snapshot in encrypted MMKV and keeps only the small encryption key in the device Keychain/Keystore. Use [`asyncStorage`](/docs/api/storage.asyncStorage) for non-sensitive state. See the [Storage reference](/docs/api/storage) for all adapters and options. # Subscriptions Subscriptions let your server charge a recurring amount after the user approves the plan once. The first request returns an HTTP `402 Payment Required` challenge, and the Tempo Accounts SDK fulfills it from the connected account. After activation, your server can serve requests during the current billing period without asking for another confirmation. When the next period is due, a worker, cron job, or request handler can collect the next payment. ## Demo By the end of this guide you'll have a protected articles route, a recurring subscription approval, and a server-side collection path for the next billing period. } className="flex flex-col gap-3"> Create an account, or use an existing one. } action={} /> The example uses a 10-second period for local testing. Production plans should use `day` or `week` periods. ## Walkthrough ::::steps ### Connect an Account Follow the [Connect Accounts](/docs/guides/connect-accounts) guide to set up Wagmi and let the user sign in. Subscriptions activate from the connected account before your server collects future periods. ### Install the Server Dependencies Install [`mppx`](https://github.com/wevm/mppx) for serving and verifying HTTP `402` payment challenges. This guide uses [Hono](https://hono.dev) on Cloudflare Workers, but the same shape works in other server runtimes. :::code-group ```bash [npm] npm i mppx hono ``` ```bash [pnpm] pnpm i mppx hono ``` ```bash [bun] bun i mppx hono ``` ::: ### Set Up `mppx` Configure `tempo.subscription` with the server account, billing currency, resolver, and store. The resolver returns the stable lookup key for the current subscriber. ```ts twoslash [worker/index.ts] // @noErrors import { Hono } from 'hono' import { Store } from 'mppx' import { Mppx, tempo } from 'mppx/hono' import { privateKeyToAccount } from 'viem/accounts' const app = new Hono() const mppx = Mppx.create({ methods: [ tempo.subscription({ account: privateKeyToAccount(process.env.ACCOUNT_PRIVATE_KEY), currency: '0x20c0000000000000000000000000000000000000', feePayer: true, resolve({ input }) { const subscriber = input.headers.get('X-Subscriber') if (!subscriber) return null return { key: `articles:${subscriber.toLowerCase()}` } }, store: Store.memory(), testnet: true, }), ], secretKey: process.env.MPP_SECRET_KEY, }) ``` :::warning\[Use a trusted lookup key] `X-Subscriber` keeps the example small. In production, derive the lookup key from a server-trusted session, API key, or user record. ::: :::warning\[Use a durable store] `Store.memory()` keeps the guide runnable. In production, use a durable store so subscription records and access keys survive deploys, restarts, and worker evictions. ::: ### Gate the Paid Route Wrap the route with `mppx.subscription`. If there is no active subscription, the middleware returns the payment challenge. After activation or renewal, the route handler runs and the response includes a `Payment-Receipt` header. ```ts twoslash [worker/index.ts] // @noErrors app.get( '/api/articles', mppx.subscription({ amount: '0.01', periodCount: 1, periodUnit: 'day', subscriptionExpires: '2030-01-01T00:00:00.000Z', }), (c) => c.json({ articles: [ { id: 1, title: 'Designing recurring payments' }, { id: 2, title: 'Collecting subscriptions in workers' }, ], }), ) ``` For local testing, set `periodCount: 10` and `periodUnit: 'dev_second'`. Production plans should use `day` or `week` periods. ### Call the Route from the Client Use `fetch` normally. The Tempo Accounts SDK handles the `402`, asks the user to approve the subscription, and retries the request with the signed credential. ```tsx twoslash [Articles.tsx] // @noErrors import { useMutation } from '@tanstack/react-query' import { stringify } from 'viem' export function Articles(props: { address: `0x${string}` }) { const articles = useMutation({ mutationFn: async () => { const res = await fetch('/api/articles', { headers: { 'X-Subscriber': props.address }, }) const body = await res.json() if (!res.ok) throw new Error(`${res.status}: ${stringify(body)}`) return body }, }) return ( ) } ``` ### Index Active Subscriptions A scheduled worker needs subscription IDs to collect. `mppx` renews a known subscription ID; your app database owns enumeration and due-time queries. You can skip this step when you only collect on the request path. `mppx.subscription` can renew an overdue subscription before serving the protected route. ### Collect from a Schedule Call `mppx.tempo.subscription.renew` from a cron worker. It returns a receipt when it collects the next period, or `null` when the subscription is already current, missing, inactive, or superseded. ```ts twoslash [worker/index.ts] // @noErrors async function collectDueSubscriptions() { for (const subscriptionId of await database.subscriptions.listDueIds()) { const result = await mppx.tempo.subscription.renew({ subscriptionId }) if (!result) continue console.log('collected subscription', { reference: result.receipt.reference, subscriptionId, }) } } export default { fetch: app.fetch, scheduled(_controller, _env, ctx) { ctx.waitUntil(collectDueSubscriptions()) }, } ``` Configure the schedule in `wrangler.jsonc`. Choose an interval that is comfortably shorter than the billing period. ```jsonc [wrangler.jsonc] { "triggers": { "crons": ["*/15 * * * *"] } } ``` :::: ## Best Practices ### Persist Subscription State Use a durable store for `tempo.subscription`. In-memory storage is only useful for local examples because subscription records and access keys disappear when the process restarts. ```ts twoslash [worker/index.ts] // @noErrors const store = createDurableSubscriptionStore(env) const mppx = Mppx.create({ methods: [ tempo.subscription({ // ... store, }), ], secretKey: process.env.MPP_SECRET_KEY, }) ``` ### Treat `renew` as Idempotent It is safe for cron to call `renew` early or more than once. A `null` result means no payment was collected, so your scheduler can continue without treating it as an error. ```ts twoslash [worker/index.ts] // @noErrors const result = await mppx.tempo.subscription.renew({ subscriptionId }) if (!result) return await database.subscriptionPayments.insert({ reference: result.receipt.reference, subscriptionId, }) ``` ### Use Server-Trusted Identity Do not trust client-provided headers for production lookup keys. Resolve subscriptions from the authenticated session, API key, organization ID, or another server-side identity that the user cannot spoof. ```ts twoslash [worker/index.ts] // @noErrors tempo.subscription({ // ... async resolve({ input }) { const session = await requireSession(input) return { key: `articles:${session.userId}` } }, }) ``` ### Keep Periods Reviewable Show the amount, currency, period, and expiry before the user approves. Use short periods only for development; production subscriptions should use stable billing periods that match the product terms. ```ts twoslash [worker/index.ts] // @noErrors app.get( '/api/articles', mppx.subscription({ amount: '10', periodCount: 1, periodUnit: 'week', subscriptionExpires: '2030-01-01T00:00:00.000Z', }), handler, ) ``` ### Monitor Collection Failures Record renewal attempts, receipts, and errors. Alert on repeated failures, expired subscriptions, unfunded accounts, and fee payer issues. ```ts twoslash [worker/index.ts] // @noErrors try { await mppx.tempo.subscription.renew({ subscriptionId }) } catch (error) { await logger.error('subscription renewal failed', { error, subscriptionId, }) throw error } ``` ## Production Checklist A durable `store` backs `mppx` subscription records. Your app database stores `subscriptionId`, lookup key, plan, next due time, and status. Collection runs from cron, a queue, or a worker scheduler. `renew` returning `null` is handled as nothing due. The fee payer is configured and its balance is monitored. Cancellation, expiry, and customer notification flows are in place. Local-only periods such as `dev_second` stay out of production. ## Next Steps # Fee Sponsorship Let connected accounts complete approved actions without keeping fee tokens on hand. Your app decides which transactions qualify for sponsorship, pays the network fee for those requests, and leaves every other transaction on the normal self-funded path. ## Demo By the end of this guide you'll have a fee sponsorship flow where the user signs in, your server decides whether to sponsor a request, and matching transfers can be routed through the server-owned fee payer. } className="flex flex-col gap-3"> } /> ## Walkthrough ::::steps ### Connect an Account Follow the [Connect Accounts](/docs/guides/connect-accounts) guide to set up Wagmi with Tempo Wallet, WebAuthn, or another signing adapter. Fee sponsorship works after the user has a connected account that can sign the transaction. ### Install the Server Dependencies Install [Hono](https://hono.dev) for the server snippets in this guide. :::code-group ```bash [npm] npm i hono ``` ```bash [pnpm] pnpm i hono ``` ```bash [bun] bun i hono ``` ::: :::tip\[Any framework works] `Handler.relay` supports the Web Fetch API and Node.js `RequestListener`, so you can mount it in Hono, Express, Next.js, Bun, Deno, Elysia, or another server framework. ::: ### Mount the Relay Create a [`Handler.relay`](/docs/server/handler.relay) with a `feePayer` account and expose it under `/relay`. The relay fills incoming transactions, signs the fee payer portion when sponsorship is approved, and returns a transaction that the connected account can sign. ```ts twoslash [worker/index.ts] // @noErrors import { Handler } from 'accounts/server' import { Hono } from 'hono' import { privateKeyToAccount } from 'viem/accounts' const app = new Hono() const relay = Handler.relay({ feePayer: { account: privateKeyToAccount(process.env.RELAY_PRIVATE_KEY), name: 'My App', url: 'https://myapp.com', }, path: '/relay', }) app.all('/relay/*', (c) => relay.fetch(c.req.raw)) export default app ``` :::tip\[Using a different framework?] `Handler.relay` exposes both `fetch` (Web Fetch API) and `listener` (Node.js `RequestListener`) entrypoints, so you can plug the same relay into any server framework: ```ts createServer(relay.listener) // Node.js Bun.serve({ fetch: relay.fetch }) // Bun Deno.serve({ fetch: relay.fetch }) // Deno app.all('*', c => relay.fetch(c.request)) // Elysia app.use(relay.listener) // Express app.use(c => relay.fetch(c.req.raw)) // Hono export const GET = relay.fetch // Next.js export const POST = relay.fetch // Next.js ``` ::: :::warning\[Add a sponsorship policy] The minimal example sponsors any transaction sent to the relay. Before funding a production fee payer account, add `feePayer.validate`, rate limits, and logging so your server only sponsors the requests your app intends to support. ::: ### Configure the Connector Pass the relay URL to your connector with `feePayer`. This tells Tempo Wallet which app relay should sponsor the transaction when signing locally or falling back to the hosted wallet UI. :::code-group ```tsx twoslash [Tempo Wallet] // @noErrors import { createConfig, http } from 'wagmi' import { tempo, tempoModerato } from 'wagmi/chains' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ chains: [tempo, tempoModerato], connectors: [ tempoWallet({ testnet: true, feePayer: import.meta.env.VITE_FEE_PAYER_URL ?? '/relay', // [!code focus] }), ], multiInjectedProviderDiscovery: false, transports: { [tempo.id]: http(), [tempoModerato.id]: http(), }, }) ``` ```tsx twoslash [Domain-bound Passkeys] // @noErrors import { createConfig, http } from 'wagmi' import { tempo, tempoModerato } from 'wagmi/chains' import { webAuthn } from 'wagmi/tempo' export const config = createConfig({ chains: [tempo, tempoModerato], connectors: [ webAuthn({ testnet: true, authUrl: '/auth', feePayer: '/relay', // [!code focus] }), ], multiInjectedProviderDiscovery: false, transports: { [tempo.id]: http(), [tempoModerato.id]: http(), }, }) ``` ::: ### Send a Transfer Call [`Hooks.wallet.useTransfer`](https://wagmi.sh/tempo/hooks/wallet.useTransfer) normally. With `feePayer` configured, the transfer uses your relay by default, where the server decides whether to sponsor it. ```tsx twoslash [Transfer.tsx] // @noErrors import { Hooks } from 'wagmi/tempo' export function Transfer() { const transfer = Hooks.wallet.useTransfer() return ( ) } ``` ### Show the Fee Payer The transfer result includes the submitted receipt. When sponsorship succeeds, `receipt.feePayer` is the server-owned account that paid the transaction fee. ```tsx twoslash [Transfer.tsx] // @noErrors import { Hooks } from 'wagmi/tempo' export function Transfer() { const transfer = Hooks.wallet.useTransfer() return (
{transfer.data?.receipt.feePayer && (

Fees paid by {transfer.data.receipt.feePayer}

)}
) } ``` :::tip\[Per-request control] The transfer above does not pass a `feePayer` override. The connector-level `feePayer` value is used by default. Only pass `feePayer: false` when you intentionally want the connected account to pay its own transaction fee. ::: :::: ## Best Practices ### Validate Sponsorship Server-Side Treat the relay as the policy boundary. Use `feePayer.validate` to check the account, target contracts, calldata, token amounts, and any app-level limits before signing. Returning `false` does not reject the transaction outright — the relay re-fills it for self-payment. ```ts twoslash [worker/index.ts] // @noErrors import { Handler } from 'accounts/server' import { privateKeyToAccount } from 'viem/accounts' const blocked = '0x0000000000000000000000000000000000000001' const handler = Handler.relay({ feePayer: { account: privateKeyToAccount('0x...'), validate: (request) => request.from?.toLowerCase() !== blocked.toLowerCase(), // [!code focus] }, }) ``` ## Next Steps # Deposits Open the Tempo deposit flow from a connected account using the [Tempo Accounts SDK](/docs). Use deposits when your app needs a user to add funds to their account. The SDK calls [`wallet_deposit`](/docs/rpc/wallet_deposit), opens the UI, and returns receipts when the selected deposit path submits account activity immediately. In the default flow, the UI asks the user to choose the destination account, amount, chain, and token. ## Demo By the end of this guide you'll have a button that opens the deposit flow without amount or token pre-filled. } className="flex flex-col gap-3"> } /> ## Walkthrough ::::steps ### Connect an Account Follow the [Connect Accounts](/docs/guides/connect-accounts) guide to let the user sign in. Deposits run after the user has a connected account that can review the request. ### Open the Deposit Flow Call [`Hooks.wallet.useDeposit`](https://wagmi.sh/tempo/hooks/wallet.useDeposit) and invoke `deposit.mutate({})`. The UI collects the deposit details from the user. ```tsx twoslash [Deposit.tsx] // @noErrors import { Hooks } from 'wagmi/tempo' export function Deposit() { const deposit = Hooks.wallet.useDeposit() return ( ) } ``` :::tip\[Optional pre-fill] If your app already knows the token or amount, pass them as hints. Omit fields when the user should choose them in the UI. ```tsx twoslash [Deposit.tsx] // @noErrors const pathUsd = '0x20c0000000000000000000000000000000000000' deposit.mutate({ amount: '10', token: pathUsd, }) ``` ::: ### Show the Result Some deposit paths return receipts immediately. Use those receipt hashes to link to the Tempo explorer or refresh balances in your app. Other paths can settle asynchronously. When `deposit.data?.receipts` is missing, show a submitted state and let your balance query update when funds arrive. ```tsx twoslash [Deposit.tsx] // @noErrors import { tempo } from 'wagmi/chains' import { Hooks } from 'wagmi/tempo' export function Deposit() { const deposit = Hooks.wallet.useDeposit() const receipt = deposit.data?.receipts?.[0] return (
{deposit.isSuccess && (
Deposit submitted. {receipt && ( See receipt )}
)}
) } ``` :::: ## Best Practices ### Buy MACH from Any Device Call `Mach.fund` with a browser, mobile, or CLI provider to request MACH checkout for the connected account. Use `Mach.getFundingUrl` when the user needs to continue on another device. Both paths preserve the intended account and requested amount. ```ts import { Mach } from 'accounts' await Mach.fund(provider, { amount: '5' }) const checkoutUrl = Mach.getFundingUrl({ address: '0x0000000000000000000000000000000000000001', amount: '5', }) ``` Checkout requires the user's confirmation. Its completion response does not by itself confirm that funds arrived. MACH is supported on Tempo mainnet; legacy MPP Credits are a separate balance. ### Deposit on Connection If adding funds is part of onboarding, show the deposit picker immediately after the user signs in. ```tsx twoslash [Connect.tsx] // @noErrors import { useConnect, useConnectors } from 'wagmi' export function Connect() { const connect = useConnect() const [connector] = useConnectors() return ( ) } ``` :::tip Use the object form when you want to pre-fill deposit details. ```tsx twoslash [Connect.tsx] // @noErrors connect.connect({ connector, capabilities: { // [!code focus] showDeposit: { // [!code focus] intent: 'mach', amount: '10', // [!code focus] token: 'MACH', // [!code focus] }, // [!code focus] }, // [!code focus] }) ``` ::: ## Next Steps # Swaps Open the Tempo swap flow from a connected account using the [Tempo Accounts SDK](/docs). Use swaps when your app needs a user to exchange one supported token for another. The SDK calls [`wallet_swap`](/docs/rpc/wallet_swap), opens the UI with any fields you pre-fill, and returns a receipt after the user confirms. You can pre-fill the token pair, amount, swap direction, and slippage. Omit fields when the user should choose them in the UI. ## Demo By the end of this guide you'll have a button that opens a pre-filled swap from pathUSD to alphaUSD. The full example lives in [`examples/swaps`](https://github.com/tempoxyz/accounts/tree/main/examples/swaps). } className="flex flex-col gap-3"> } /> ## Walkthrough ::::steps ### Connect an Account Follow the [Connect Accounts](/docs/guides/connect-accounts) guide to let the user sign in. Swaps run after the user has a connected account that can confirm the request. ### Open the Swap Flow Call [`Hooks.wallet.useSwap`](https://wagmi.sh/tempo/hooks/wallet.useSwap) and pass the fields your app already knows. The user still reviews and confirms the swap in the UI. ```tsx twoslash [Swap.tsx] // @noErrors import { Hooks } from 'wagmi/tempo' const pathUsd = '0x20c0000000000000000000000000000000000000' const alphaUsd = '0x20c0000000000000000000000000000000000001' export function Swap() { const swap = Hooks.wallet.useSwap() return ( ) } ``` ### Show the Result When the swap succeeds, the hook returns the submitted receipt. Use the receipt hash to link to the block explorer or refresh balances in your app. ```tsx twoslash [Swap.tsx] // @noErrors import { tempoModerato } from 'wagmi/chains' import { Hooks } from 'wagmi/tempo' export function Swap() { const swap = Hooks.wallet.useSwap() return (
{swap.isSuccess && ( See receipt )}
) } ``` :::: ## Best Practices ### Choose the Direction Carefully Use `type: 'sell'` when `amount` is the exact amount of `token` the user is selling. Use `type: 'buy'` when `amount` is the exact amount of `token` the user wants to receive. ```tsx twoslash [Swap.tsx] // @noErrors swap.mutate({ amount: '1', pairToken: alphaUsd, token: pathUsd, type: 'sell', }) ``` ### Keep Slippage Explicit Set `slippage` only when your UI explains the tolerance to the user. The value is a decimal fraction, so `0.01` means 1%. ```tsx twoslash [Swap.tsx] // @noErrors swap.mutate({ amount: '1', pairToken: alphaUsd, slippage: 0.01, token: pathUsd, type: 'sell', }) ``` ### Surface Pending and Error State Disable duplicate submissions while the wallet prompt is open, and surface `swap.error` when the request is rejected or fails. ```tsx twoslash [Swap.tsx] // @noErrors import { Hooks } from 'wagmi/tempo' export function Swap() { const swap = Hooks.wallet.useSwap() return (
{swap.error && (
{`${swap.error.name}: ${swap.error.message}`}
)}
) } ``` ## Next Steps # Theming Theming lets you tune Tempo Wallet's embedded account prompts without changing your signing adapter or account flow. Use it when your app already uses Tempo Wallet and you want the prompt to match your product's accent color, corner radius, and light or dark mode. These options apply to hosted Tempo Wallet surfaces opened by the `dialog` or `tempoWallet` adapter. App-owned buttons, forms, and custom adapter screens still use your own design system. ## Walkthrough ::::steps ### Connect an Account Follow the [Connect Accounts](/docs/guides/connect-accounts) guide to set up Wagmi with the Tempo Wallet adapter and let the user sign in. Theming is configured on the same connector, so you do not need to change your app components. ### Choose Theme Values Pick the values that should travel with the hosted prompt. | Option | Values | Purpose | | -------- | ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | | `accent` | `'neutral'`, `'blue'`, `'red'`, `'amber'`, `'green'`, `'purple'`, or any CSS color value | Colors primary actions and emphasis states. | | `radius` | `'none'`, `'small'`, `'medium'`, `'large'`, or `'full'` | Controls the corner radius used inside the prompt. | | `scheme` | `'light'` or `'dark'` | Sets the prompt's color scheme. Omit it to follow the user's operating system setting. | ### Configure Tempo Wallet Pass `theme` to `tempoWallet`. The SDK sends these values to the embedded iframe or popup whenever Tempo Wallet opens. ```tsx twoslash [src/config.ts] // @noErrors import { createConfig, http } from 'wagmi' import { tempo } from 'wagmi/chains' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ chains: [tempo], connectors: [ tempoWallet({ theme: { // [!code focus] accent: '#ff007a', // [!code focus] radius: 'full', // [!code focus] }, // [!code focus] }), ], multiInjectedProviderDiscovery: false, transports: { [tempo.id]: http(), }, }) ``` ### Use the Same Sign In Flow Your sign-in button stays the same. When `connect` opens Tempo Wallet, the prompt uses the theme from your connector configuration. ```tsx twoslash [src/Connect.tsx] // @noErrors import { useConnect, useConnectors } from 'wagmi' export function Connect() { const connect = useConnect() const [connector] = useConnectors() return ( ) } ``` :::: ## Next Steps # CLI Build a command-line tool that asks a user to approve a scoped access key in the browser, then signs future commands locally from the terminal. The CLI provider uses the Wata device-code flow (OAuth 2.0 Device Authorization Grant, RFC 8628). The command registers an access-key request, opens a Tempo Wallet approval page, polls until the user approves it, and stores the signed key in provider storage. After that, matching transfers or paid API calls can run without reopening the browser until the key expires. ## Example By the end of this guide, you will have a CLI that connects an account, authorizes a one-day pathUSD permission, sends a $1 transfer, and fetches a paid API endpoint. Run the example from the repo: ```bash pnpm -F cli-example start ``` The runnable source lives in [`examples/cli`](https://github.com/tempoxyz/accounts/tree/main/examples/cli). ## Walkthrough ::::steps ### Install Dependencies Install `accounts` together with Viem. The example uses [`incur`](https://github.com/wevm/incur) for the command wrapper and `ox` for hex helpers, but you can swap in your own CLI framework. :::code-group ```bash [npm] npm i accounts viem ox incur ``` ```bash [pnpm] pnpm i accounts viem ox incur ``` ```bash [bun] bun i accounts viem ox incur ``` ::: ### Create the CLI Provider Create a [`Provider`](/docs/cli/provider) from `accounts/cli`. The example opts into testnet defaults. Machine Payment Protocol support is enabled by default, so ordinary `fetch` calls can answer `402 Payment Required` challenges. ```ts twoslash [cli.ts] import { Provider } from 'accounts/cli' const provider = Provider.create({ testnet: true, }) ``` ### Set Up Incur Set up [`incur`](https://github.com/wevm/incur) with a `my-cli connect` command. The command body can start small; the next step adds the account authorization. ```ts twoslash [cli.ts] import { Cli } from 'incur' import { Provider } from 'accounts/cli' const provider = Provider.create({ testnet: true, }) const cli = Cli.create('my-cli') cli.command('connect', { description: 'Connect a Tempo account', async run() { const client = provider.getClient() // ... }, }) cli.serve() ``` ### Authorize an Access Key Call `connect` with `capabilities.authorizeAccessKey` inside the `connect` command. The CLI adapter requires this capability during `wallet_connect`, because it needs a signed local key before it can keep working after the browser approval closes. The policy below matches the example: the user approves up to 100 pathUSD for one day, and only for the two token calls the CLI intends to make. ```ts twoslash [cli.ts] // @noErrors import { Expiry } from 'accounts' import { Hex } from 'ox' import { parseUnits } from 'viem' import { connect } from 'viem/experimental/erc7846' const pathUsd = '0x20c0000000000000000000000000000000000000' cli.command('connect', { description: 'Connect a Tempo account', async run() { const client = provider.getClient() return connect(client, { capabilities: { authorizeAccessKey: { expiry: Expiry.days(1), limits: [ { limit: Hex.fromNumber(parseUnits('100', 6)), token: pathUsd, }, ], scopes: [ { address: pathUsd, selector: 'transfer(address,uint256)' }, { address: pathUsd, selector: 'transferWithMemo(address,uint256,bytes32)' }, ], }, }, }) }, }) ``` When this runs, the provider registers a device code, opens the verification URL, and polls the token endpoint until Tempo Wallet returns the signed key authorization. ### Submit a Transfer Read the active account from the provider, then submit a transfer that fits the approved policy. The local managed key signs the request, and the key authorization is included with the transaction. ```ts twoslash [cli.ts] // @noErrors import { parseUnits } from 'viem' import { Actions } from 'viem/tempo' cli.command('send', { description: 'Send 1 pathUSD to the connected account', async run() { const account = provider.getAccount() const client = provider.getClient() return Actions.token.transferSync(client, { account, amount: parseUnits('1', 6), to: account.address, token: pathUsd, }) }, }) ``` ### Fetch a Paid API The provider installs a payment-aware `fetch` wrapper in Node.js by default. If an endpoint responds with an MPP charge, the CLI signs the payment locally and retries the request. ```ts twoslash [cli.ts] cli.command('ping', { description: 'Fetch a paid endpoint', async run() { const response = await fetch('https://mpp.dev/api/ping/paid') const data = await response.text() return { ping: data, } }, }) ``` :::: ## Best Practices ### Scope Every Key Always pair `limits` with `scopes`. Limits define how much the key can spend, while scopes define which calls it can make. Start narrow and ask the user to approve a new key when the command needs a broader policy. ```ts twoslash [cli.ts] import { Expiry } from 'accounts' import { Hex } from 'ox' import { parseUnits } from 'viem' const pathUsd = '0x20c0000000000000000000000000000000000000' const authorizeAccessKey = { expiry: Expiry.days(1), limits: [ { limit: Hex.fromNumber(parseUnits('100', 6)), token: pathUsd, }, ], scopes: [ { address: pathUsd, selector: 'transfer(address,uint256)' }, ], } ``` ### Use App-Specific Storage Set a filesystem storage path for production CLIs so managed keys are isolated by app, environment, and account. The default path is convenient for local experiments, but a named config path makes support and revocation easier. ```ts twoslash [cli.ts] import { Provider, Storage } from 'accounts/cli' const provider = Provider.create({ storage: Storage.filesystem({ path: '~/.config/example/tempo-store.json', }), }) ``` ### Use a Durable Code Store If you host your own CLI approval flow, mount the [`wata`](https://github.com/wevm/wata) device-code host transport and back it with durable storage. A memory store is fine for local development, but production device codes should survive process restarts while they are pending. ```ts twoslash [server.ts] // @noErrors import { Store, Wata, deviceCode } from 'wata/host' const wata = Wata.create({ transports: [ deviceCode({ html: { render, authenticate }, path: '/auth/device', store: Store.cloudflare(env.KV), }), ], }) ``` ## Next Steps # Adapters ## Overview Adapters decide where keys live, who runs authentication, and how account requests are approved. Your app talks to the Tempo Accounts SDK through one provider surface while the adapter handles signing. | Adapter | Who owns auth | Best for | | ---------------------------------------------- | ------------------- | ----------------------------------------------------------------------------------- | | [Tempo Wallet](/docs/adapters/tempo-wallet) | Tempo Wallet | Apps that want a universal wallet without owning signing infrastructure. | | [WebAuthn (Passkeys)](/docs/adapters/webauthn) | Your app origin | Teams that want domain-bound passkeys and Tempo account features. | | [Turnkey](/docs/adapters/turnkey) | Turnkey | Apps that want Turnkey to own key custody and authentication. | | [Privy](/docs/adapters/privy) | Privy | Apps that want Privy auth and embedded wallets with Tempo account features. | | [Private Key](/docs/adapters/private-key) | Your environment | Server-side signers with a pinned key, or development with a random in-storage key. | | [Custom](/docs/adapters/custom) | Your infrastructure | AWS KMS, internal signers, and other hosted wallet products. | ```mermaid sequenceDiagram participant App participant SDK as Tempo Accounts SDK participant Adapter participant Signer App->>SDK: JSON-RPC request SDK->>Adapter: route request Adapter->>Signer: authenticate and sign Signer-->>Adapter: signed result Adapter-->>SDK: response SDK-->>App: response ``` ## Next Steps # Tempo Wallet Adapter Tempo Wallet is the hosted adapter for apps that want embedded account creation and signing without owning auth, passkeys, or wallet infrastructure. Use it when you want: * A universal account that can move across apps. * Hosted approval surfaces through iframe or popup dialogs. * Tempo-managed passkey and signing infrastructure. * A fast path for app teams that do not need to build a wallet. ## Usage :::code-group ```tsx twoslash [Web (Wagmi)] import { createConfig, http } from 'wagmi' import { tempo } from 'wagmi/chains' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ chains: [tempo], connectors: [tempoWallet()], transports: { [tempo.id]: http(), }, }) ``` ```ts twoslash [Web (Vanilla)] import { Provider, tempoWallet } from 'accounts' const provider = Provider.create({ adapter: tempoWallet(), }) ``` ```ts twoslash [React Native] import { Provider } from 'accounts' import { tempoWallet } from 'accounts/mobileWebAuth' import { openAuthSession } from 'accounts/react-native/expo-web-browser' const provider = Provider.create({ adapter: tempoWallet({ baseUrl: 'https://app.example', openAuthSession, redirectUri: 'com.example.app:/auth', }), }) ``` ```ts twoslash [CLI] import { Provider } from 'accounts/cli' const provider = Provider.create() ``` ::: ## Mobile On mobile there is no embedded dialog. The adapter connects through [mobile web auth](/docs/api/mobileWebAuth): each request opens Tempo Wallet in a system browser session and completes via an app-link callback to `redirectUri`. Tempo Wallet identifies your app through the consumer discovery document served at `baseUrl`, and that document must list the exact `redirectUri`. See [Consumer Discovery](/docs/api/mobileWebAuth#consumer-discovery) for the document format, and the [React Native guide](/docs/guides/react-native) for platform setup. ## Next Steps # WebAuthn Adapter The WebAuthn adapter is for apps and enterprises that want account keys bound to their own domain. Your app owns the passkey ceremony and the Tempo Accounts SDK plugs those signatures into Tempo account flows. Use it when you want: * Passkeys created on your own origin. * Your app to own registration and authentication UX. * Tempo account features without using the hosted Tempo Wallet surface. ## Usage :::code-group ```tsx twoslash [Web (Wagmi)] import { createConfig, http } from 'wagmi' import { tempo } from 'wagmi/chains' import { webAuthn } from 'wagmi/tempo' export const config = createConfig({ chains: [tempo], connectors: [webAuthn({ authUrl: '/auth' })], transports: { [tempo.id]: http(), }, }) ``` ```ts twoslash [Web (Vanilla)] import { Provider, webAuthn } from 'accounts' const provider = Provider.create({ adapter: webAuthn({ auth: '/auth' }), }) ``` ```ts twoslash [CLI] import { Provider } from 'accounts/cli' const provider = Provider.create() ``` ::: ## How Passkeys Work WebAuthn has two ceremonies: registration creates a new credential, and authentication proves that the same credential is present later. In both cases, your server sends a challenge, the browser asks the authenticator to sign it, and your server verifies the signed response with the stored public key. The browser mediates the UI, so your app never handles the raw private key material directly. Passkeys can live on a platform authenticator such as Face ID, Touch ID, or Windows Hello, or on a roaming authenticator such as a hardware security key. The private key stays on the device or credential provider and is not exported through the WebAuthn API. :::info Use `webAuthn({ auth: '/auth' })` when you want the browser to talk to a server backed ceremony. If you need to plug in your own ceremony implementation, use `WebAuthnCeremony.server({ url })` or `WebAuthnCeremony.local({ storage })`. ::: ## Setting up your Server For a server backed flow, mount [`Handler.webAuthn`](/docs/server/handler.webAuthn) and point the client adapter at the same path. The handler exposes `POST /auth/register/options`, `POST /auth/register`, `POST /auth/login/options`, `POST /auth/login`, and `POST /auth/logout`. Use a persistent [`Kv`](/docs/server/kv) store in production so challenges, credentials, and sessions survive deploys and restarts. :::code-group ```ts twoslash [Server] import { Handler, Kv } from 'accounts/server' export const handler = Handler.webAuthn({ kv: Kv.memory(), origin: 'https://app.example.com', rpId: 'example.com', }) createServer(handler.listener) // Node.js Bun.serve({ fetch: handler.fetch }) // Bun Deno.serve({ fetch: handler.fetch }) // Deno app.all('*', c => handler.fetch(c.request)) // Elysia app.use(handler.listener) // Express app.use(c => handler.fetch(c.req.raw)) // Hono export const GET = handler.fetch // Next.js export const POST = handler.fetch // Next.js ``` ```ts twoslash [Client] import { Provider, webAuthn } from 'accounts' const provider = Provider.create({ adapter: webAuthn({ auth: '/auth' }), }) ``` ::: :::warning `Kv.memory()` is fine for local development and tests, but not for production. Use a persistent `Kv` adapter instead. [Learn more](/docs/server/kv). ::: ## Learn More ### Origin and RP ID WebAuthn credentials are scoped to an origin and an RP ID. `origin` is the full browser origin that must match during verification, while `rpId` is the effective domain the credential is bound to. In development, `http://localhost` works because browsers allow WebAuthn on localhost as a secure context exception. Choose `rpId` carefully before you ship. If you register with `rpId: 'example.com'`, that credential can be used from `app.example.com` and other subdomains that satisfy WebAuthn's RP rules. But the reverse is not true: a passkey created with `rpId: 'app.example.com'` cannot later be used with `rpId: 'example.com'`. ```ts twoslash import { Handler, Kv } from 'accounts/server' const handler = Handler.webAuthn({ kv: Kv.memory(), origin: 'https://app.example.com', rpId: 'example.com', }) ``` :::warning Treat `rpId` like part of your public API. Changing it later can strand existing credentials and force users to register new passkeys. ::: ### Sessions A successful login automatically provisions a session for the authenticated user. By default, that session is issued as a cookie and downstream routes can read it without any extra plumbing. For non-browser callers, the login response can also return a `{ token }` body so the same session can be carried as a bearer token. For downstream routes on the same server, call `handler.getSession(req)` to resolve the current WebAuthn session. It reads the session from the cookie or from an `Authorization: Bearer ` header and returns `undefined` when the request is not authenticated. ```ts twoslash // @noErrors import { Handler, Kv } from 'accounts/server' const handler = Handler.webAuthn({ kv: Kv.memory(), origin: 'https://app.example.com', rpId: 'example.com', }) app.get('/me', async (req) => { const session = await handler.getSession(req) if (!session) return new Response('unauthorized', { status: 401 }) return Response.json({ credentialId: session.credentialId }) }) ``` :::info You can disable automatic session provisioning by passing `session: false` to `Handler.webAuthn`. With sessions disabled, login no longer issues a cookie or a token, the `/auth/logout` endpoint is not mounted, and `getSession` always returns `undefined`. ```ts twoslash import { Handler, Kv } from 'accounts/server' export const handler = Handler.webAuthn({ kv: Kv.memory(), origin: 'https://app.example.com', rpId: 'example.com', session: false, // [!code focus] }) ``` ::: ### Account Recovery Passkeys are bound to authenticators, so recovery is different from password reset. Some credentials sync through OS or password manager providers such as iCloud Keychain, Google Password Manager, or 1Password, but you should not assume every user has sync enabled or access to the same provider on every device. A user can also lose all registered authenticators at once. Plan for multiple credentials per user from the start. Let users add another passkey from a second device or a hardware key after they sign in, and make that visible in account settings. If losing every authenticator would lock a user out forever, you need a separate recovery flow with stronger checks and explicit support tooling. :::warning Do not treat possession of one device as permanent account recovery. Recovery should be deliberate, audited, and separate from normal login. ::: See the [WebAuthn production checklist](/docs/production?adapter=web-authn) before shipping. ## Next Steps # Turnkey Adapter The Turnkey adapter is for apps that want Turnkey to own key custody and authentication while plugging into Tempo account features. Your app keeps the login or sign-up UX (passkey prompts, OTP, etc.); the adapter handles silent reconnect, session expiry, and provider signing. Use it when you want: * Turnkey to own key custody and policy. * Passkey or other Turnkey-supported authentication on your own organization. * Tempo account features without managing your own signer infrastructure. ## Usage ```tsx import { TurnkeyClient, generateWalletAccountsFromAddressFormat } from '@turnkey/core' import { Provider, turnkey } from 'accounts' const provider = Provider.create({ adapter: turnkey({ client: new TurnkeyClient({ organizationId, authProxyConfigId }), createAccount: async ({ client, parameters }) => { await client.signUpWithPasskey({ passkeyDisplayName: parameters.name, createSubOrgParams: { userName: parameters.name, customWallet: { walletName: 'FooBar', walletAccounts: generateWalletAccountsFromAddressFormat({ addresses: ['ADDRESS_FORMAT_ETHEREUM'], }), }, }, }) }, loadAccounts: async ({ client }) => { await client.loginWithPasskey() }, }), }) ``` ## Human Pass Needed This page should be expanded with sub-org setup, session lifetime guidance, recovery flows, and production policy expectations. ## Next Steps # Privy Adapter The Privy adapter is for React apps that want Privy to own authentication, session state, and embedded wallets while plugging into Tempo account features. Use `privy` from `accounts/react/privy`; it wires Privy's hooks into the Accounts SDK and signs through Privy's embedded Ethereum provider. Use it when you want: * Privy to own app authentication and embedded wallets. * Your React app to keep using Privy's hosted login UI. * Tempo account features backed by Privy's raw `secp256k1_sign` embedded-wallet signer. ## Usage Wrap your app with `PrivyProvider`, configure embedded Ethereum wallet creation, and mount `PrivyAccountsBridge` inside the same React tree. Then create the provider with `privy()`. :::code-group ```tsx [main.tsx] import { PrivyProvider } from '@privy-io/react-auth' import { PrivyAccountsBridge } from 'accounts/react/privy' import { createRoot } from 'react-dom/client' import { App } from './App.js' createRoot(document.getElementById('root')!).render( , ) ``` ```ts [provider.ts] import { Provider } from 'accounts' import { privy } from 'accounts/react/privy' export const provider = Provider.create({ adapter: privy(), }) ``` ::: :::tip `PrivyAccountsBridge` is a React boundary. It exists because Privy's `usePrivy`, `useWallets`, and `useLogin` hooks can only run under `PrivyProvider`, while the Accounts SDK provider is plain TypeScript. ::: ## Account Behavior With `privy()`, all embedded Ethereum wallets on the Privy user are connected. Privy does not expose secp256k1 public keys for embedded Ethereum wallets, so the adapter cannot construct a `viem/tempo` `Account` directly from the connected Privy account. Instead, it signs Tempo payloads through Privy's embedded Ethereum provider with `secp256k1_sign`. The first embedded Ethereum wallet returned by Privy is active by default. ## Implementation Note The public Privy integration lives at `accounts/react/privy`. Internally, it wraps the lower-level Privy adapter with a hook-backed client so app code does not need to manage a separate adapter bridge. ## Next Steps # Private Key Adapter The Private Key adapter signs in-process with a `secp256k1` private key. Use it when you have a key the host environment owns (server-side signers, fee payers, scripts) or when you need a quick development signer that persists a generated key in storage. Use it when you want: * A private key supplied by your environment (e.g. server-side signers, scripted flows). * A development-only signer that generates and persists a random key in the provider's storage. * A lightweight signing layer without passkey or hosted wallet UX. :::warning Storing keys in browser storage in plaintext is dangerous — only omit `privateKey` for development, testing, or when the threat model allows it. ::: ## Usage ```ts twoslash import { Provider, secp256k1 } from 'accounts' const provider = Provider.create({ adapter: secp256k1({ privateKey: '0x...' }), }) ``` ## Learn more ### Generating a Private Key You need a 32-byte secret to use as `privateKey`. Generate it once, store it in a secret manager, and load it from an environment variable at runtime. Never commit the value to the repo. :::code-group ```bash [CLI] cast wallet new ``` ```ts twoslash [TypeScript] import { Secp256k1 } from 'viem/tempo' const privateKey = Secp256k1.randomPrivateKey() ``` ::: :::warning Treat the output of either command as a production secret from the moment it is generated. Do not paste it into chat, tickets, or commit history. ::: ### Server-side Signers Use `secp256k1({ privateKey })` only in trusted server code — API routes, workers, cron jobs, fee payers, or scripted signers. Load the key from an environment variable or secret manager, fail fast if it is missing, and never expose it to browser bundles. ```ts twoslash import { Provider, secp256k1 } from 'accounts' export const provider = Provider.create({ adapter: secp256k1({ privateKey: '0x...' }), }) ``` Use separate keys for separate roles. A fee payer, background automation signer, and emergency admin signer should not share one private key. Rotate by deploying a new secret, moving traffic or jobs to the new signer, then retiring the old key. Never print the key, never include it in logs or error payloads, and never send it to client code. :::warning Passing a `privateKey` is full custody. Treat it like any other production secret. ::: ### Development and Testing For local playgrounds and one-off demos, `secp256k1()` is the quickest path. On first connect it generates a random key, then persists it through the provider storage adapter such as `localStorage` or cookies because `secp256k1` wraps [`local`](/docs/api/local). That is useful for development, but it is not a safe default for real user custody. Browser storage is plaintext to your app and can leak through XSS, shared machines, debug tooling, or accidental persistence. For automated tests, prefer a deterministic key instead of a generated one. That keeps addresses and fixtures stable across runs. In CI, inject a dedicated test key with an environment variable rather than committing one to the repo. ```ts twoslash import { Provider, secp256k1 } from 'accounts' const provider = Provider.create({ adapter: secp256k1({ privateKey: '0x...' }), }) ``` ### When Not to Use This Do not use the generated browser form for production end-user wallets unless your threat model explicitly accepts plaintext key persistence. If you need domain-bound user authentication and non-exportable keys, prefer [WebAuthn](/docs/adapters/webauthn). If you want hosted custody or auth-managed embedded wallets, prefer [Turnkey](/docs/adapters/turnkey) or [Privy](/docs/adapters/privy). Also avoid sharing one long-lived private key across unrelated environments. Development, staging, production, and CI should each use different keys so a leak in one environment does not compromise the others. See the [Private Keys production checklist](/docs/production?adapter=private-keys) before shipping. ## Next Steps # Custom Adapter The Tempo Accounts SDK exposes a low-level `Adapter.define` API for authoring custom adapters. Use it when none of the built-in adapters ([`tempoWallet`](/docs/adapters/tempo-wallet), [`webAuthn`](/docs/adapters/webauthn), [`turnkey`](/docs/adapters/turnkey), [`privy`](/docs/adapters/privy), [`secp256k1`](/docs/adapters/private-key)) match your custody model. Use it when you want: * A signer not covered by a built-in adapter. * Full control over the JSON-RPC actions exposed to the provider. * A thin wrapper that adds metadata or middleware around an existing adapter. :::tip If you use Turnkey or Privy, start with their built-in adapters before writing a custom adapter. They already cover session restore, account loading, provider signing, and the callback shape for app-owned auth. ::: :::tip If your signer can produce a private key or sign arbitrary digests, consider wrapping the [`local`](/docs/api/local) adapter first — it implements every required action for you and only asks you to supply `createAccount` and `loadAccounts`. Reach for `Adapter.define` directly when you need to override signing, request translation, or session lifecycle behavior. ::: ## Anatomy An adapter is a function annotated with display metadata. `Adapter.define(meta, setup)` returns that function, the provider calls it once at startup, and the returned `Instance` wires actions into the provider's `request()` dispatcher. ```ts import { Adapter } from 'accounts' const myAdapter = Adapter.define( { name: 'My Wallet', rdns: 'com.example.mywallet', icon: 'data:image/svg+xml;base64,...', }, ({ getAccount, getClient, storage, store }) => { return { actions: { async createAccount(params, request) { // ... }, async loadAccounts(params, request) { // ... }, async signPersonalMessage({ address, data }) { // ... }, async signTypedData({ address, data }) { // ... }, async signTransaction(params, request) { // ... }, async sendTransaction(params, request) { // ... }, async sendTransactionSync(params, request) { // ... }, }, cleanup() { // ... }, } }, ) ``` ## Error mapping Wrap signer-specific errors in [`ox`](https://oxlib.sh)'s `Provider` errors so app code can handle them without knowing your signer's internals. ```ts twoslash import { Provider as ox_Provider } from 'ox' declare function callMySigner(): Promise try { await callMySigner() } catch (error) { // Translate auth failures into the provider contract. throw new ox_Provider.UserRejectedRequestError(error as Error) } ``` Common mappings: * User-cancelled prompts → `ox_Provider.UserRejectedRequestError`. * Missing or expired session → `ox_Provider.UnauthorizedError`. * Methods you choose not to implement → `ox_Provider.UnsupportedMethodError`. * Malformed adapter input → `ox_Provider.InvalidParamsError`. ## Reference adapters The built-in adapters are good blueprints when you build your own. ## Next Steps # Adapters Adapters control how accounts are created and managed. Pass an adapter to `Provider.create()` to configure the account management strategy. | Adapter | Uses | Best for | | ------------------------------------------ | ---------------------------------- | --------------------------------------------------------------------------------------------------- | | [`dialog`](/docs/api/dialog) (default) | Hosted dialog | Apps wanting a universal account experience | | [`mobileWebAuth`](/docs/api/mobileWebAuth) | Mobile browser auth session | React Native apps and mobile clients connecting to a hosted wallet | | [`postMessage`](/docs/api/postMessage) | Wallet page popup | Web apps connecting to a hosted wallet through a popup session | | [`webAuthn`](/docs/api/webAuthn) | Domain-bound passkeys | Wallets integrating Passkey accounts, or Apps wanting to host and manage their own Passkey accounts | | [`turnkey`](/docs/api/turnkey) | `@turnkey/core` client sessions | Apps using Turnkey-managed wallet accounts | | [`privy`](/docs/api/privy) | Privy React Auth | Apps using Privy auth and embedded wallets | | [`secp256k1`](/docs/api/secp256k1) | In-process `secp256k1` private key | Server-side signers with a pinned key, or development with a random in-storage key | | [`local`](/docs/api/local) | Arbitrary keys | Custom signing and key management | *** # `dialog` Delegates signing to an external-origin iframe or popup dialog over the bespoke messenger. **Deprecated** in favor of the [`postMessage`](/docs/api/postMessage) adapter (`tempoWallet`), which uses the standard uRPC window transport. ## Usage ```ts twoslash import { dialog, Provider } from 'accounts' const provider = Provider.create({ adapter: dialog(), }) ``` ## Parameters ### dialog * **Type:** `Dialog` * **Default:** `Dialog.iframe()` (or `Dialog.popup()` in Safari/insecure contexts) Dialog to use for the embed app. ```ts twoslash import { dialog, Dialog, Provider } from 'accounts' const provider = Provider.create({ adapter: dialog({ dialog: Dialog.popup(), // [!code focus] }), }) ``` ### host * **Type:** `string` * **Default:** `'https://wallet.tempo.xyz/embed'` URL of the embed app. ```ts twoslash import { dialog, Provider } from 'accounts' const provider = Provider.create({ adapter: dialog({ host: 'https://wallet.tempo.xyz/embed', // [!code focus] }), }) ``` ### icon * **Type:** `` `data:image/${string}` `` * **Optional** Data URI of the provider icon. ```ts twoslash import { dialog, Provider } from 'accounts' const provider = Provider.create({ adapter: dialog({ icon: 'data:image/svg+xml,...', // [!code focus] }), }) ``` ### name * **Type:** `string` * **Default:** `'Tempo Wallet'` Display name of the provider. ```ts twoslash import { dialog, Provider } from 'accounts' const provider = Provider.create({ adapter: dialog({ name: 'My Wallet', // [!code focus] }), }) ``` ### rdns * **Type:** `string` * **Default:** `'xyz.tempo'` Reverse DNS identifier for [EIP-6963](https://eips.ethereum.org/EIPS/eip-6963) provider discovery. ```ts twoslash import { dialog, Provider } from 'accounts' const provider = Provider.create({ adapter: dialog({ rdns: 'com.example.wallet', // [!code focus] }), }) ``` ### theme * **Type:** `Dialog.Theme` * **Optional** Visual theme overrides applied to the wallet dialog. Supports an `accent` color (preset or CSS value), a `radius` preset (`'none' | 'small' | 'medium' | 'large' | 'full'`), and a `scheme` (`'light' | 'dark'`). Defaults follow the user's OS color scheme. ```ts twoslash import { dialog, Provider } from 'accounts' const provider = Provider.create({ adapter: dialog({ theme: { // [!code focus] accent: 'purple', // [!code focus] radius: 'medium', // [!code focus] scheme: 'dark', // [!code focus] }, // [!code focus] }), }) ``` # `local` Creates a local adapter where the app manages keys and signing in-process. Use this when you need full control over account creation and transaction signing. ## Usage ```ts import { local, Provider } from 'accounts' const provider = Provider.create({ adapter: local({ loadAccounts: async () => ({ accounts: [{ address: '0x...' }], }), }), }) ``` ## Parameters ### createAccount * **Type:** `(params: Adapter.createAccount.Parameters) => Promise` * **Optional** Create a new account. Omit for login-only flows. ```ts declare function createAccount(parameters: { /** Access key to grant during the ceremony. */ authorizeAccessKey?: Adapter.authorizeAccessKey.Parameters | undefined /** Digest the caller asked the wallet to sign during the create-account ceremony. */ digest?: Hex | undefined /** Display name for the new account (e.g. credential name for WebAuthn). */ name: string /** * EIP-191 message folded into the create-account ceremony. The wallet hashes it * and signs it in the same prompt; conflicts with a caller-supplied `digest`. */ personalSign?: { message: string } | undefined /** Opaque user identifier (e.g. for WebAuthn `user.id`). */ userId?: string | undefined }): Promise<{ /** Accounts created by the ceremony (required). */ accounts: readonly Account[] /** Email associated with the account. */ email?: string | null | undefined /** Signed key authorization, if an access key was granted. */ keyAuthorization?: KeyAuthorization.Rpc | undefined /** Echo of the caller-supplied `personalSign` message. */ personalSign?: { message: string } | undefined /** Signature over the digest. The adapter signs the digest itself if you don't return one. */ signature?: Hex | undefined /** Username associated with the account. */ username?: string | null | undefined }> ``` ```ts twoslash import { local, Provider } from 'accounts' const provider = Provider.create({ adapter: local({ loadAccounts: async () => ({ accounts: [{ address: '0x...' }] }), createAccount: async (params) => ({ // [!code focus] accounts: [{ address: '0x...' }], // [!code focus] }), // [!code focus] }), }) ``` ### icon * **Type:** `` `data:image/${string}` `` * **Optional** Data URI of the provider icon. ```ts twoslash import { local, Provider } from 'accounts' const provider = Provider.create({ adapter: local({ loadAccounts: async () => ({ accounts: [{ address: '0x...' }] }), icon: 'data:image/svg+xml,...', // [!code focus] }), }) ``` ### loadAccounts * **Type:** `(params?: Adapter.loadAccounts.Parameters | undefined) => Promise` * **Required** Discover existing accounts (e.g. via WebAuthn assertion or key lookup). ```ts declare function loadAccounts(parameters?: { /** Access key to grant during the ceremony. */ authorizeAccessKey?: Adapter.authorizeAccessKey.Parameters | undefined /** Restrict authentication to one or more credentials (WebAuthn). */ credentialId?: string | readonly string[] | undefined /** Digest the caller asked the wallet to sign during the load-accounts ceremony. */ digest?: Hex | undefined /** * EIP-191 message folded into the load-accounts ceremony. * Conflicts with a caller-supplied `digest`. */ personalSign?: { message: string } | undefined /** Prompt the user to pick from all available credentials instead of the last-used one. */ selectAccount?: boolean | undefined }): Promise<{ /** Loaded accounts (required). */ accounts: readonly Account[] /** Email associated with the account. */ email?: string | null | undefined /** Signed key authorization, if an access key was granted. */ keyAuthorization?: KeyAuthorization.Rpc | undefined /** Echo of the caller-supplied `personalSign` message. */ personalSign?: { message: string } | undefined /** Signature over the digest. The adapter signs the digest itself if you don't return one. */ signature?: Hex | undefined /** Username associated with the account. */ username?: string | null | undefined }> ``` ```ts twoslash import { local, Provider } from 'accounts' const provider = Provider.create({ adapter: local({ loadAccounts: async (params) => ({ // [!code focus] accounts: [{ address: '0x...' }], // [!code focus] }), // [!code focus] }), }) ``` ### name * **Type:** `string` * **Default:** `'Injected Wallet'` Display name of the provider. ```ts twoslash import { local, Provider } from 'accounts' const provider = Provider.create({ adapter: local({ loadAccounts: async () => ({ accounts: [{ address: '0x...' }] }), name: 'My Wallet', // [!code focus] }), }) ``` ### rdns * **Type:** `string` * **Default:** `` `com.{lowercase name}` `` Reverse DNS identifier. ```ts twoslash import { local, Provider } from 'accounts' const provider = Provider.create({ adapter: local({ loadAccounts: async () => ({ accounts: [{ address: '0x...' }] }), rdns: 'com.example.wallet', // [!code focus] }), }) ``` # `mobileWebAuth` Creates an adapter that sends provider RPC requests from a mobile to a web host using the [uRPC Mobile Web Auth transport](https://github.com/tempoxyz/urpc/blob/main/specs/transport-mobile-web-auth.md). Each request is a single exchange: one auth URL carries one RPC request, and one callback URL carries one RPC response. The wallet identifies the app through the consumer discovery document served at `baseUrl`; `name` and `rdns` only describe the provider identity announced to the app. Both adapters are exported from the root entry — `mobileWebAuth` and `tempoWalletMobileWebAuth` — for bundlers that tree-shake. React Native (Metro) apps should import from the `accounts/mobileWebAuth` subpath instead, since Metro does not tree-shake unused root exports. ## Usage ```ts twoslash import { Provider } from 'accounts' import { mobileWebAuth } from 'accounts/mobileWebAuth' import { openAuthSession } from 'accounts/react-native/expo-web-browser' const provider = Provider.create({ adapter: mobileWebAuth({ baseUrl: 'https://app.example', host: 'https://wallet.example', name: 'Example Wallet', openAuthSession, rdns: 'com.example.wallet', redirectUri: 'com.example.app:/auth', }), }) ``` ## tempoWallet `tempoWallet()` is a template over `mobileWebAuth()` preconfigured with the Tempo Wallet host and provider identity. It accepts `baseUrl`, `redirectUri`, `openAuthSession`, and optional `fetch`. ```ts twoslash import { Provider } from 'accounts' import { tempoWallet } from 'accounts/mobileWebAuth' import { openAuthSession } from 'accounts/react-native/expo-web-browser' const provider = Provider.create({ adapter: tempoWallet({ baseUrl: 'https://app.example', openAuthSession, redirectUri: 'com.example.app:/auth', }), }) ``` ## Consumer Discovery Mobile web auth uses two discovery origins: * `baseUrl`: consumer discovery origin serving `/.well-known/urpc/consumer.json` — your app's origin * `host`: wallet discovery origin serving `/.well-known/urpc/host.json` The wallet identifies your app through the consumer document served at `baseUrl`, and the document must list the exact `redirectUri` passed to the adapter. ```json { "callback_urls": [ "com.example.app:/auth", "com.example.app://auth" ], "id": "app.example", "name": "Example App", "origin": "https://app.example", "version": "1.0" } ``` Use a reverse-DNS private-use callback URI registered to your app. Browser-managed callback URLs — such as those issued by Expo Go — are not valid mobile web auth callbacks. ## Parameters ### baseUrl * **Type:** `string` * **Required** Public HTTPS origin that serves your app's [consumer discovery document](#consumer-discovery). ### fetch * **Type:** `typeof fetch` * **Optional** Override the fetch implementation used for discovery and auth-session requests. ### host * **Type:** `string | object` * **Required** Wallet discovery origin, or a preloaded host document. ### name * **Type:** `string` * **Required** Provider display name. ### openAuthSession * **Type:** `({ authorizationUrl, callback }) => string | undefined | Promise` * **Required** Opens the auth URL with a native browser auth session and returns the callback URL. ```ts twoslash import { openAuthSession } from 'accounts/react-native/expo-web-browser' ``` ### rdns * **Type:** `string` * **Required** Reverse-DNS provider identifier. ### redirectUri * **Type:** `string` * **Required** Callback URI watched by the auth session. Publish the exact URI in the [consumer discovery document](#consumer-discovery). # `postMessage` Connect to a wallet hosted on another domain from anywhere on the web. Communication happens over the [uRPC window](https://github.com/tempoxyz/urpc/blob/main/specs/transport-window.md) transport. ## Usage ```ts twoslash import { Provider } from 'accounts' import { postMessage } from 'accounts/postMessage' const provider = Provider.create({ adapter: postMessage({ host: 'https://wallet.example/post-message', name: 'Example Wallet', rdns: 'com.example.wallet', }), }) ``` ## tempoWallet To connect to the Tempo Wallet, use the preconfigured [`tempoWallet`](#tempowallet) below. ````ts twoslash import { Provider, tempoWallet } from 'accounts' const provider = Provider.create({ adapter: tempoWallet(), }) ## Parameters ### host - **Type:** `string` - **Required by:** `postMessage` URL of the wallet's post-message page. The adapter tags the URL with your app's origin — so the wallet can pin its responses before the first frame arrives — and the mount mode, so approvals render matching chrome. ### mount - **Type:** `Mount.Factory` - **Optional** Where the wallet page lives and how it surfaces for requests. Defaults to `Mount.auto()` — an overlay iframe, or a popup where iframes can't work. ### name - **Type:** `string` - **Required by:** `postMessage` Provider display name. ### rdns - **Type:** `string` - **Required by:** `postMessage` Reverse-DNS provider identifier. ## Mounts The mount decides where the wallet page lives and how it surfaces for requests. By default the adapter picks for you: an overlay iframe, falling back to a popup in insecure contexts or when the browser lacks IntersectionObserver v2 (the wallet can't run its occlusion defense). On Safari, account requests use a temporary popup because Safari rejects WebAuthn creation inside cross-origin iframes. If the wallet detects its iframe is occluded mid-session, it asks to continue in a popup and the adapter remounts there, replaying the request. Pass a mount to pin one explicitly: ```ts twoslash import { popup, postMessage } from 'accounts/postMessage' const adapter = postMessage({ host: 'https://wallet.example/post-message', mount: popup({ size: { height: 440, width: 360 } }), name: 'Example Wallet', rdns: 'com.example.wallet', }) ```` # `privy` Creates a Privy adapter for React apps using `@privy-io/react-auth`. Import it from `accounts/react/privy`. The adapter opens Privy's login UI through a React boundary, reads Privy's hook state, and returns embedded Ethereum wallets to the Accounts SDK. Privy does not expose secp256k1 public keys for embedded Ethereum wallets, so the adapter signs Tempo payloads through Privy's embedded provider with `secp256k1_sign`. ## Usage ```ts import { Provider } from 'accounts' import { privy } from 'accounts/react/privy' export const provider = Provider.create({ adapter: privy(), }) ``` The helper expects `PrivyAccountsBridge` under `PrivyProvider`: ```tsx import { PrivyProvider } from '@privy-io/react-auth' import { PrivyAccountsBridge } from 'accounts/react/privy' root.render( , ) ``` The bridge resolves connect requests with Privy's embedded Ethereum wallets. ## Parameters ### icon * **Type:** `` `data:image/${string}` `` * **Optional** Data URI of the provider icon. ```ts import { Provider } from 'accounts' import { privy } from 'accounts/react/privy' const provider = Provider.create({ adapter: privy({ icon: 'data:image/svg+xml,...', // [!code focus] }), }) ``` ### name * **Type:** `string` * **Default:** `'Privy'` Display name of the provider. ```ts import { Provider } from 'accounts' import { privy } from 'accounts/react/privy' const provider = Provider.create({ adapter: privy({ name: 'My Wallet', // [!code focus] }), }) ``` ### rdns * **Type:** `string` * **Default:** `'io.privy'` Reverse DNS identifier. ```ts import { Provider } from 'accounts' import { privy } from 'accounts/react/privy' const provider = Provider.create({ adapter: privy({ rdns: 'com.example.wallet', // [!code focus] }), }) ``` ## PrivyAccountsBridge `PrivyAccountsBridge` is a null-rendering React component. Mount it under `PrivyProvider` and inside the same React tree as the app that creates or uses the Accounts SDK provider. It exists because Privy's `usePrivy`, `useWallets`, and `useLogin` hooks can only run inside `PrivyProvider`, while the Accounts SDK provider is plain TypeScript. ## Account Behavior The public React adapter connects all embedded Ethereum wallets returned by Privy. During connect, the bridge returns those wallet addresses to the internal adapter. The internal adapter materializes each embedded wallet provider with Privy's SDK and signs with `provider.request({ method: 'secp256k1_sign', params: [hash] })`. Because Privy does not expose the Ethereum wallet public key, the adapter does not construct a `viem/tempo` `Account` directly from the connected Privy account. # `secp256k1` Creates an adapter that signs in-process with a `secp256k1` private key. If `privateKey` is provided, the adapter pins that key as the signer (useful for server-side use, where the key is supplied by the host environment). If `privateKey` is omitted, the adapter generates a random key on first connect and persists it via the provider's storage adapter (e.g. `localStorage`, cookies). :::warning Storing keys in browser storage in plaintext is dangerous — only use the unpinned form for development, testing, or when the threat model allows it. ::: Wraps the [`local`](/docs/api/local) adapter. ## Usage ```ts twoslash import { secp256k1, Provider } from 'accounts' const provider = Provider.create({ adapter: secp256k1({ privateKey: '0x...' }), }) ``` ## Parameters ### icon * **Type:** `` `data:image/${string}` `` * **Optional** Data URI of the provider icon. ```ts twoslash import { secp256k1, Provider } from 'accounts' const provider = Provider.create({ adapter: secp256k1({ icon: 'data:image/svg+xml,...', // [!code focus] }), }) ``` ### name * **Type:** `string` * **Default:** `'Injected Wallet'` Display name of the provider. ```ts twoslash import { secp256k1, Provider } from 'accounts' const provider = Provider.create({ adapter: secp256k1({ name: 'My Wallet', // [!code focus] }), }) ``` ### privateKey * **Type:** `Hex` * **Optional** Fixed private key to expose instead of generating/loading one from storage. When provided, the adapter pins this key for every connect. ```ts twoslash import { secp256k1, Provider } from 'accounts' const provider = Provider.create({ adapter: secp256k1({ privateKey: '0x...', // [!code focus] }), }) ``` ### rdns * **Type:** `string` * **Default:** `` `com.{lowercase name}` `` Reverse DNS identifier. ```ts twoslash import { secp256k1, Provider } from 'accounts' const provider = Provider.create({ adapter: secp256k1({ rdns: 'com.example.wallet', // [!code focus] }), }) ``` # `turnkey` Creates a Turnkey adapter backed by `@turnkey/core` client sessions and Ethereum wallet accounts. The adapter owns silent reconnect, session-expiry cleanup, and provider signing actions. Apps usually provide one UI-bearing login or sign-up flow through [`loadAccounts`](#loadaccounts). Use [`createAccount`](#createaccount) only when registration needs a distinct Turnkey flow. ## Usage ```ts import { TurnkeyClient, generateWalletAccountsFromAddressFormat } from '@turnkey/core' import { Provider, turnkey } from 'accounts' const provider = Provider.create({ adapter: turnkey({ client: new TurnkeyClient({ organizationId, authProxyConfigId }), createAccount: async ({ client, parameters }) => { await client.signUpWithPasskey({ passkeyDisplayName: parameters.name, createSubOrgParams: { userName: parameters.name, customWallet: { walletName: 'FooBar', walletAccounts: generateWalletAccountsFromAddressFormat({ addresses: ['ADDRESS_FORMAT_ETHEREUM'], }), }, }, }) }, loadAccounts: async ({ client }) => { await client.loginWithPasskey() }, }), }) ``` ## Parameters ### client * **Type:** `turnkey.Client` * **Required** Existing Turnkey client, such as `TurnkeyClient` from `@turnkey/core`. The adapter calls `client.init?.()` once on first use, and uses `getSession`, `fetchWallets`, `httpClient.signRawPayload`, and `logout` for the rest of the lifecycle. ```ts import { TurnkeyClient } from '@turnkey/core' import { Provider, turnkey } from 'accounts' const provider = Provider.create({ adapter: turnkey({ client: new TurnkeyClient({ organizationId, authProxyConfigId }), // [!code focus] loadAccounts: async ({ client }) => { /* ... */ }, }), }) ``` ### icon * **Type:** `` `data:image/${string}` `` * **Optional** Data URI of the provider icon. ```ts import { Provider, turnkey } from 'accounts' const provider = Provider.create({ adapter: turnkey({ client, loadAccounts: async () => { /* ... */ }, icon: 'data:image/svg+xml,...', // [!code focus] }), }) ``` ### loadAccounts * **Type:** `(params: { client; parameters? }) => Promise` * **Required** Loads/logs into existing Turnkey wallet accounts. UI is allowed (e.g. passkey prompts to start a new session). Receives the initialized Turnkey `client` and the optional provider load-accounts `parameters` (e.g. `authorizeAccessKey`, `digest`, `personalSign`, `selectAccount`). After this callback completes, the adapter calls `client.fetchWallets()` and exposes fetched Ethereum accounts. By default all the user's Ethereum accounts will be connected. Return `Address[]` to select and order the fetched accounts. The first returned address is active by default, matching `eth_requestAccounts`. When `createAccount` is omitted, register requests call `loadAccounts` too. This fits email and social auth flows where the same Turnkey ceremony can sign in an existing user or create one. ```ts import { Provider, turnkey } from 'accounts' const provider = Provider.create({ adapter: turnkey({ client, loadAccounts: async ({ client }) => { // [!code focus] await client.loginWithPasskey() // [!code focus] }, // [!code focus] }), }) ``` ```ts import { Provider, turnkey } from 'accounts' const provider = Provider.create({ adapter: turnkey({ client, loadAccounts: async ({ client }) => { await client.loginWithPasskey() return [selectedAddress] // [!code focus] }, }), }) ``` ### createAccount * **Type:** `(params: { client; parameters }) => Promise` * **Optional** * **Default:** `loadAccounts` Creates/registers a Turnkey wallet account when registration needs a separate flow. Receives the initialized Turnkey `client` and the provider create-account `parameters` (display name, optional `digest`, `personalSign`, `userId`, etc.). After this callback completes, the adapter calls `client.fetchWallets()` and exposes fetched Ethereum accounts. Like `loadAccounts`, by default all the user's Ethereum accounts will be connected. Return `Address[]` to choose the returned accounts. The first address is active by default. ```ts import { generateWalletAccountsFromAddressFormat } from '@turnkey/core' import { Provider, turnkey } from 'accounts' const provider = Provider.create({ adapter: turnkey({ client, createAccount: async ({ client, parameters }) => { // [!code focus] await client.signUpWithPasskey({ // [!code focus] passkeyDisplayName: parameters.name, // [!code focus] createSubOrgParams: { // [!code focus] userName: parameters.name, // [!code focus] customWallet: { // [!code focus] walletName: 'FooBar', // [!code focus] walletAccounts: generateWalletAccountsFromAddressFormat({ // [!code focus] addresses: ['ADDRESS_FORMAT_ETHEREUM'], // [!code focus] }), // [!code focus] }, // [!code focus] }, // [!code focus] }) // [!code focus] }, // [!code focus] loadAccounts: async ({ client }) => { /* ... */ }, }), }) ``` ### name * **Type:** `string` * **Default:** `'Turnkey'` Display name of the provider. ```ts import { Provider, turnkey } from 'accounts' const provider = Provider.create({ adapter: turnkey({ client, loadAccounts: async () => { /* ... */ }, name: 'My Wallet', // [!code focus] }), }) ``` ### rdns * **Type:** `string` * **Default:** `'com.turnkey'` Reverse DNS identifier. ```ts import { Provider, turnkey } from 'accounts' const provider = Provider.create({ adapter: turnkey({ client, loadAccounts: async () => { /* ... */ }, rdns: 'com.example.wallet', // [!code focus] }), }) ``` ### sessionSkewMs * **Type:** `number` * **Default:** `10000` Milliseconds before Turnkey session expiry to proactively disconnect. Used both when scheduling the auto-disconnect timer and when deciding whether the current session is still valid for signing. ```ts import { Provider, turnkey } from 'accounts' const provider = Provider.create({ adapter: turnkey({ client, loadAccounts: async () => { /* ... */ }, sessionSkewMs: 30_000, // [!code focus] }), }) ``` # `webAuthn` Enables domain-bound passkey experiences by wrapping the `local` adapter with WebAuthn registration and authentication flows. ## Usage ```ts import { webAuthn, Provider } from 'accounts' const provider = Provider.create({ adapter: webAuthn({ auth: '/auth' }), }) ``` ## Parameters `ceremony` and `auth` are mutually exclusive — the `Options` type is constrained as `OneOf<{ ceremony } | { auth }>`. ### auth * **Type:** `string | { url?: string; challenge?: string; verify?: string; logout?: string; returnToken?: boolean }` * **Optional** Server Authentication endpoint for WebAuthn ceremonies (shorthand for `WebAuthnCeremony.server({ url }){:js}`). Accepts the same shape as the [Provider `auth` capability](/docs/api/provider#auth) so the same value can be passed to both surfaces; the WebAuthn adapter only consumes the `url` field — SIWE-only fields (`challenge`, `verify`, `logout`, `returnToken`) are ignored by the ceremony. When supplied (and `ceremony` is omitted), the adapter falls back to `WebAuthnCeremony.server({ url }){:js}`. :::warning Cannot be used with `ceremony`. ::: ```ts twoslash import { webAuthn, Provider } from 'accounts' const provider = Provider.create({ adapter: webAuthn({ auth: '/auth', // [!code focus] }), }) ``` ### ceremony * **Type:** `WebAuthnCeremony` * **Default:** `WebAuthnCeremony.local({ storage }){:js}` when `auth` is also omitted, otherwise `WebAuthnCeremony.server({ url }){:js}`. Ceremony strategy for WebAuthn registration and authentication. [See more](/docs/api/webauthnceremony). :::warning Cannot be used with `auth`. ::: ```ts twoslash import { webAuthn, WebAuthnCeremony, Provider } from 'accounts' const provider = Provider.create({ adapter: webAuthn({ ceremony: WebAuthnCeremony.server({ url: '/auth' }), // [!code focus] }), }) ``` ### icon * **Type:** `` `data:image/${string}` `` * **Optional** Data URI of the provider icon. ```ts twoslash import { webAuthn, Provider } from 'accounts' const provider = Provider.create({ adapter: webAuthn({ auth: '/auth', icon: 'data:image/svg+xml,...', // [!code focus] }), }) ``` ### name * **Type:** `string` * **Default:** `'Injected Wallet'` Display name of the provider. ```ts twoslash import { webAuthn, Provider } from 'accounts' const provider = Provider.create({ adapter: webAuthn({ auth: '/auth', name: 'My Wallet', // [!code focus] }), }) ``` ### rdns * **Type:** `string` * **Default:** `` `com.{lowercase name}` `` Reverse DNS identifier. ```ts twoslash import { webAuthn, Provider } from 'accounts' const provider = Provider.create({ adapter: webAuthn({ auth: '/auth', rdns: 'com.example.wallet', // [!code focus] }), }) ``` # Dialog Dialog modes control how the Tempo Wallet is embedded into your app. Pass a dialog to the [`dialog()`](/docs/api/dialog) adapter to configure the embed mode. ## Modes # `Dialog.iframe` Creates an iframe dialog that embeds the auth app in a `` element. This is the default on most browsers in secure (HTTPS) contexts. The instance is a **host-keyed singleton** — calling `Dialog.iframe()` repeatedly with the same `host` returns the cached instance and swaps in the latest store/theme rather than re-mounting the iframe. Switching to a different host tears down the previous iframe and creates a fresh one. Visual appearance is configured via [`Dialog.Theme`](/docs/api/dialogs#dialogtheme), passed through the [`dialog`](/docs/api/dialog) adapter. ## Usage ```ts import { dialog, Dialog, Provider } from 'accounts' const provider = Provider.create({ adapter: dialog({ dialog: Dialog.iframe() }), }) ``` ## Fallback behavior The iframe instance falls back to a popup in the following cases, all triggered from `syncRequests`: * **Safari** — when a request uses `wallet_connect` or `eth_requestAccounts`. Safari does not support WebAuthn credential creation inside cross-origin iframes, so these methods are forwarded to a popup. * **Untrusted host without IntersectionObserver v2** — when the host page is not in the wallet's trusted hosts list and the browser does not support IntersectionObserver v2 (used to detect iframe occlusion / clickjacking). A warning is logged and the request is forwarded to a popup. To opt the iframe back in, add your hostname to the trusted hosts list. * **Mode switch from the auth app** — if the embedded auth app sends a `switch-mode` message, the iframe is hidden and any pending requests are forwarded to the popup for the remainder of the session. In SSR environments (no `window`), `Dialog.iframe()` returns `Dialog.noop()`. # `Dialog.popup` Opens the auth app in a new browser window. Used as the default on Safari and insecure (HTTP) origins. ## Usage ```ts import { dialog, Dialog, Provider } from 'accounts' const provider = Provider.create({ adapter: dialog({ dialog: Dialog.popup() }), }) ``` ## Parameters Accepts a [`Dialog.popup.Options`](https://github.com/tempoxyz/accounts/blob/main/src/core/Dialog.ts) object. ### size * **Type:** `{ width: number; height: number } | undefined` * **Default:** [`Dialog.defaultSize`](/docs/api/dialogs#dialogdefaultsize) — `{ width: 360, height: 440 }` Popup window dimensions. # `Expiry` Utility functions that return a Unix timestamp (seconds) offset from the current time. Useful for setting access key expiry. ## Usage ```ts import { Expiry } from 'accounts' // Access key that expires in 1 day const expiry = Expiry.days(1) ``` ## Functions ### Expiry.seconds(n) Returns a Unix timestamp `n` seconds from now. ### Expiry.minutes(n) Returns a Unix timestamp `n` minutes from now. ### Expiry.hours(n) Returns a Unix timestamp `n` hours from now. ### Expiry.days(n) Returns a Unix timestamp `n` days from now. ### Expiry.weeks(n) Returns a Unix timestamp `n` weeks from now. ### Expiry.months(n) Returns a Unix timestamp `n` months (30 days) from now. ### Expiry.years(n) Returns a Unix timestamp `n` years (365 days) from now. # `Provider` Creates an EIP-1193 provider with a pluggable adapter for managing accounts on Tempo. The returned object implements [EIP-1193](https://eips.ethereum.org/EIPS/eip-1193) (`request`, `on`, `removeListener`, `emit`, ...) and exposes a few convenience helpers (`getAccount`, `getAccessKeyStatus`, `getClient`, `getMppxParameters`, `store`, `chains`). See [Return Type](#return-type) for the full surface. ## Usage ```ts twoslash import { Provider } from 'accounts' const provider = Provider.create() ``` ### Using JSON-RPC Methods You can interact with accounts via the provider's `request` method. ```ts twoslash // @noErrors import { Provider } from 'accounts' const provider = Provider.create() const { accounts } = await provider.request({ // [!code focus] method: 'wallet_connect', // [!code focus] }) // [!code focus] ``` ### Using Viem Actions If you prefer to use Viem actions over raw JSON-RPC calls, the provider exposes a `getClient` accessor. ```ts twoslash // @noErrors import { parseUnits } from 'viem' import { tempoActions } from 'viem/tempo' import { Provider } from 'accounts' const provider = Provider.create() const client = provider.getClient().extend(tempoActions()) // [!code focus] const { receipt } = await client.token.transferSync({ // [!code focus] amount: parseUnits('100', 6), // [!code focus] to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb', // [!code focus] token: '0x20c0000000000000000000000000000000000001', // [!code focus] }) // [!code focus] ``` ### Access Key Status Use `getAccessKeyStatus` to check whether the active account has a matching delegated key before starting a paid or delegated flow. ```ts twoslash // @noErrors import { Provider } from 'accounts' const provider = Provider.create() const status = await provider.getAccessKeyStatus({ // [!code focus] calls: [{ // [!code focus] to: '0x20c0000000000000000000000000000000000001', // [!code focus] data: '0xa9059cbb', // [!code focus] }], // [!code focus] }) // [!code focus] if (status === 'pending') { // The next matching transaction will publish the key authorization. } ``` ## Parameters ### accessKey * **Type:** `{ authorize?: Provider.create.AuthorizeAccessKey; keystores?: Keystore.Keystores }` * **Optional** Access-key configuration: `authorize` automatically authorizes an access key when no stored key satisfies a request; `keystores` is where key material lives. #### accessKey.authorize * **Type:** `Provider.create.AuthorizeAccessKeyParameters | (() => Provider.create.AuthorizeAccessKeyParameters | undefined)` * **Optional** Access-key parameters to authorize automatically when no stored key satisfies a request. Applies to `wallet_connect` and transaction sends: the provider first checks stored access keys and reuses one that satisfies the request; if none does, `wallet_connect` includes `capabilities.authorizeAccessKey`, and sends authorize the key immediately beforehand when the requested scopes cover the transaction calls. Pass an object to use the same parameters for every request, or a function to compute them per request — return `undefined` to skip authorization for that request. The optional `reuse` object is SDK-only. It is never sent to the wallet RPC. Use it to make reuse stricter than the default scope check. Tempo Wallet-hosted approval pages require at least one `limits` entry and one `scopes` entry. Local adapters accept unbounded keys, but production apps should pass both so local and wallet-hosted flows use the same policy. ```ts declare function authorize(): { /** Access key address. Alternative to `publicKey` when the caller already knows the derived address. */ address?: Address | undefined /** Chain ID the key authorization is scoped to. Defaults to the active chain. */ chainId?: bigint | undefined /** Unix timestamp (seconds) when the key expires. */ expiry: number /** Key type of the external public key. Required when `publicKey` or `address` is provided. */ keyType?: 'secp256k1' | 'p256' | 'webAuthn' | undefined /** TIP-20 spending limits. Optional `period` (seconds) makes the limit roll over each period. */ limits?: { limit: bigint; period?: number; token: Address }[] | undefined /** Private key material to keep in SDK storage. It is not forwarded to wallets over JSON-RPC. */ privateKey?: Hex | undefined /** * External public key to authorize. When provided, no key pair is generated — * the caller holds the signing material. */ publicKey?: Hex | undefined /** Call scopes restricting which contracts/selectors this key can call. */ scopes?: | { address: Address; recipients?: readonly Address[]; selector?: Hex | string }[] | undefined /** SDK-only reuse policy. Omitted before `wallet_connect` or `wallet_authorizeAccessKey` RPCs. */ reuse?: { /** Minimum Unix timestamp (seconds) a stored key must be valid through. */ minExpiry?: number | undefined /** Minimum spending limits a stored key must satisfy. */ minLimits?: { limit: bigint; period?: number; token: Address }[] | undefined } | undefined } | undefined ``` ```ts twoslash import { parseUnits } from 'viem' import { Expiry, Provider } from 'accounts' const provider = Provider.create({ accessKey: { // [!code focus] authorize: { // [!code focus] expiry: Expiry.days(7), // [!code focus] limits: [{ // [!code focus] token: '0x20c0000000000000000000000000000000000001', // [!code focus] limit: parseUnits('500', 6), // [!code focus] period: 60 * 60 * 24, // rolling daily limit // [!code focus] }], // [!code focus] scopes: [{ // [!code focus] address: '0x20c0000000000000000000000000000000000001', // [!code focus] selector: 'transfer(address,uint256)', // [!code focus] }], // [!code focus] reuse: { // [!code focus] minExpiry: Expiry.hours(1), // [!code focus] minLimits: [{ // [!code focus] token: '0x20c0000000000000000000000000000000000001', // [!code focus] limit: parseUnits('50', 6), // [!code focus] period: 60 * 60 * 24, // [!code focus] }], // [!code focus] }, // [!code focus] }, // [!code focus] }, // [!code focus] }) ``` If `reuse` is omitted, the provider reuses any unexpired locally signable key whose scopes cover the requested scopes and transaction calls. Add `reuse.minExpiry` or `reuse.minLimits` when a stored key must also be valid long enough or have enough remaining policy capacity for your app's workflow. You can also pre-derive the access key locally by passing `publicKey` or `address` with `keyType`. The wallet still authorizes that key during `wallet_connect`; the app keeps the signing material. For `keyType: 'secp256k1'`, generate and store the private key in your app or backend, then pass either the derived `address` or the `publicKey` in `authorizeAccessKey`. Alternatively, pass `privateKey` to let the SDK derive the access-key address and persist the private key through the configured [`storage`](#storage). `privateKey` material is not forwarded to wallets over JSON-RPC. ```ts twoslash // @noErrors import { Expiry, Provider } from 'accounts' const provider = Provider.create({ accessKey: { // [!code focus] authorize: { // [!code focus] expiry: Expiry.days(7), // [!code focus] keyType: 'p256', // [!code focus] publicKey: '0x1234', // [!code focus] }, // [!code focus] }, // [!code focus] }) ``` #### accessKey.keystores * **Type:** `Keystore.Keystores` * **Default:** `Keystore.defaults` — `{ p256: Keystore.webCryptoP256() }` [Keystores](/docs/api/keystore) backing provider-generated access keys, one per key type. ```ts twoslash import { Keystore, Provider } from 'accounts' const provider = Provider.create({ accessKey: { // [!code focus] keystores: { p256: Keystore.webCryptoP256({ extractable: true }) }, // [!code focus] }, // [!code focus] }) ``` ### adapter * **Type:** `Adapter` * **Default:** `dialog()` Adapter to use for account management. ```ts twoslash import { Provider, webAuthn } from 'accounts' const provider = Provider.create({ adapter: webAuthn({ auth: '/auth' }), // [!code focus] }) ``` ### auth * **Type:** `string | { url?: string; challenge?: string; verify?: string; logout?: string; returnToken?: boolean }` * **Optional** Default Server Authentication configuration for `wallet_connect`. When set, every `wallet_connect` call orchestrates the SIWE round-trip against this endpoint unless the caller passes their own `capabilities.auth` (per-call override). Designed to point at a service running [`Handler.webAuthn`](/docs/server/handler.webAuthn) from `accounts/server`. Pass a base URL string (endpoints are derived as `${url}/challenge`, `${url}` for verify, and `${url}/logout`), or an object with explicit endpoints. ```ts twoslash import { Provider } from 'accounts' const provider = Provider.create({ auth: '/api/auth', // [!code focus] }) ``` Or with explicit endpoints: ```ts twoslash import { Provider } from 'accounts' const provider = Provider.create({ auth: { // [!code focus] challenge: '/api/auth/challenge', // [!code focus] verify: '/api/auth/verify', // [!code focus] logout: '/api/auth/logout', // [!code focus] returnToken: true, // [!code focus] }, // [!code focus] }) ``` ### chains * **Type:** `readonly [Chain, ...Chain[]]` * **Default:** `[tempo, tempoModerato, tempoDevnet]` Supported chains. The first chain is the default. ```ts twoslash import { Provider } from 'accounts' import { tempo } from 'viem/tempo/chains' const provider = Provider.create({ chains: [tempo], // [!code focus] }) ``` ### feePayer * **Type:** `string | false | { url: string; precedence?: 'fee-payer-first' | 'user-first' }` * **Optional** Fee payer configuration for interacting with a service running [`Handler.relay`](/docs/server/handler.relay) from `accounts/server`. Pass a URL string, an object with `url` and optional `precedence` to control whether the fee payer or the user pays first, or `false` to opt out. ```ts twoslash import { Provider } from 'accounts' const provider = Provider.create({ feePayer: 'https://myapp.com/fee-payer', // [!code focus] }) ``` Or with precedence: ```ts twoslash import { Provider } from 'accounts' const provider = Provider.create({ feePayer: { // [!code focus] url: 'https://myapp.com/fee-payer', // [!code focus] precedence: 'user-first', // [!code focus] }, // [!code focus] }) ``` ### identity * **Type:** `{ audience?: string; issuer: string }` * **Optional** Identity (verified email) token minting for local adapters. When a `wallet_connect` request asks for `identity.email` and the adapter returns no identity claims — local adapters have no wallet host to mint them — the provider mints the id token from this issuer's `/token` route (the `Handler.oidcProvider` shape) and attaches it to the connect result. The issuer must trust body-supplied subjects (`Handler.oidcProvider` without `authenticate`), so this is for development deployments — for example, signing in to a local app with the `secp256k1` adapter and an auto-verified email. * `audience` — the `aud` claim bound into minted tokens. Defaults to `location.origin`. * `issuer` — the OIDC issuer whose `/token` route mints the id token. ```ts twoslash import { Provider, secp256k1 } from 'accounts' const provider = Provider.create({ adapter: secp256k1(), identity: { issuer: 'http://localhost:8787/v1/auth/dev-oidc' }, // [!code focus] }) ``` ### maxAccounts * **Type:** `number` * **Optional** Maximum number of accounts to persist. Oldest accounts are evicted when exceeded (LRU). ```ts twoslash import { Provider } from 'accounts' const provider = Provider.create({ maxAccounts: 5, // [!code focus] }) ``` ### mpp * **Type:** `boolean | Provider.mpp.Options` * **Default:** `true` Enable [Machine Payment Protocol](https://mpp.dev) (mppx) support. Enabled by default — pass an options object to configure, or `false` to disable. ```ts twoslash import { Provider } from 'accounts' const provider = Provider.create({ mpp: false, // [!code focus] }) ``` MPP session endpoints can use `deposit` or `maxDeposit` so the client opens and tops up a payment channel before sending vouchers. Use `provider.getMppxParameters()` when composing an mppx client with `tempo({ account, ...provider.getMppxParameters() })`. ```ts twoslash import { Provider } from 'accounts' const provider = Provider.create({ mpp: { // [!code focus] maxDeposit: '10', // [!code focus] mode: 'pull', // [!code focus] }, // [!code focus] }) ``` ### persistCredentials * **Type:** `boolean` * **Default:** `true` Whether to persist credentials and access keys to storage. When `false`, only account addresses are persisted (the user must re-authenticate on each session). ```ts twoslash import { Provider } from 'accounts' const provider = Provider.create({ persistCredentials: false, // [!code focus] }) ``` ### relay * **Type:** `string` * **Optional** Base URL for a wallet relay endpoint. When set, every chain's transport defaults to `http("${relay}/${chainId}")` — a single endpoint that routes by chain ID via the path. Per-chain entries in `transports` override this on a chain-by-chain basis. Designed to point at a service running [`Handler.relay`](/docs/server/handler.relay) from `accounts/server`. Pass the base path here. The provider appends the active chain ID, so a base path of `/relay` sends chain `33139` requests to `/relay/33139`. ```ts twoslash import { Provider } from 'accounts' const provider = Provider.create({ relay: '/relay', // [!code focus] // tempo (33139) → http('/relay/33139') // tempoModerato → http('/relay/') }) ``` ### storage * **Type:** `Storage` * **Default:** `Storage.idb()` in browser, `Storage.memory()` otherwise Storage adapter for persistence. The default uses IndexedDB in browsers (via `Storage.idb()`) and an in-memory map elsewhere. ```ts twoslash import { Provider, Storage } from 'accounts' const provider = Provider.create({ storage: Storage.memory(), // [!code focus] }) ``` ### testnet * **Type:** `boolean` * **Default:** `false` Use testnet. When `true`, the default chain will be the first testnet chain in `chains`. ```ts twoslash import { Provider } from 'accounts' const provider = Provider.create({ testnet: true, // [!code focus] }) ``` ### transports * **Type:** `Record` * **Optional** Per-chain transports keyed by chain ID. When omitted, defaults to `http()` for each chain (uses the chain's default RPC URL). Layered on top of `relay`, so explicit per-chain entries win over `relay`-derived defaults. ```ts twoslash import { Provider } from 'accounts' import { http } from 'viem' import { tempo, tempoModerato } from 'viem/tempo/chains' const provider = Provider.create({ transports: { // [!code focus] [tempo.id]: http('/relay/' + tempo.id), // [!code focus] [tempoModerato.id]: http('/relay/' + tempoModerato.id), // [!code focus] }, // [!code focus] }) ``` ## Return Type The provider type is exported as `Provider` and is the intersection of [`ox.Provider.Provider`](https://oxlib.sh/Provider) (the EIP-1193 surface) and [`ox.Provider.Emitter`](https://oxlib.sh/Provider#emitter) plus a few extra accessors: ```ts import type { Address, Hex, Provider as ox_Provider } from 'ox' import type { tempo as mppx_tempo } from 'mppx/client' import type { Chain, Client as ViemClient, Transport } from 'viem' import type { JsonRpcAccount } from 'viem/accounts' import type { tempo } from 'viem/tempo/chains' export type Provider = ox_Provider.Provider & ox_Provider.Emitter & { /** Configured chains. */ chains: readonly [Chain, ...Chain[]] /** Returns the active root account as a viem JSON-RPC account. */ getAccount(): JsonRpcAccount /** Returns local or on-chain publication status for an access key. */ getAccessKeyStatus(options?: { address?: Address | undefined accessKey?: Address | undefined calls?: readonly { to?: Address | undefined; data?: Hex | undefined }[] | undefined chainId?: number | undefined }): Promise<'missing' | 'pending' | 'published' | 'expired'> /** Returns a viem Client for the given (or current) chain ID. */ getClient(options?: { chainId?: number | undefined feePayer?: string | undefined }): ViemClient /** Returns mppx Tempo client parameters backed by this provider. */ getMppxParameters(): { getClient: NonNullable resolveAccount: NonNullable } /** Reactive state store. */ store: Store } ``` # `Rpc` A namespace of Zod schemas describing every JSON-RPC method the [`Provider`](/docs/api/provider) handles, alongside the shared building blocks (logs, receipts, key authorizations, transaction requests) those methods compose from. Each RPC method is exposed as its own sub-namespace with a `schema` (a [`Schema.Item`](/docs/api/schema#item)) and `Encoded` / `Decoded` types describing the wire and runtime shapes. The aggregated schema is re-exported as [`Schema.schema`](/docs/api/schema#schema) for use with [`Provider`](/docs/api/provider). For end-user JSON-RPC documentation (parameters, return values, examples), see the [JSON-RPC reference](/docs/rpc/wallet_connect). ## Usage ```ts twoslash import { Rpc } from 'accounts' const item = Rpc.eth_chainId.schema type Decoded = Rpc.wallet_connect.Decoded ``` ## Method Namespaces Each method namespace exposes a uniform shape: ```ts namespace eth_accounts { /** Zod schema for the method (a `Schema.Item`). */ const schema: Schema.Item /** Wire-format type — raw JSON-RPC `{ method, params, returns }`. */ type Encoded /** Decoded type — the runtime shape after codec transforms. */ type Decoded } ``` The full list of method namespaces is: * `eth_accounts` * `eth_chainId` * `eth_fillTransaction` * `eth_requestAccounts` * `eth_sendTransaction` * `eth_sendTransactionSync` * `eth_signTransaction` * `eth_signTypedData_v4` * `personal_sign` * `wallet_authorizeAccessKey` * `wallet_connect` * `wallet_deposit` * `wallet_depositZone` * `wallet_disconnect` * `wallet_getBalances` * `wallet_getCallsStatus` * `wallet_getCapabilities` * `wallet_revokeAccessKey` * `wallet_send` * `wallet_sendCalls` * `wallet_swap` * `wallet_switchEthereumChain` * `wallet_withdrawZone` ```ts twoslash import { Rpc } from 'accounts' const { schema } = Rpc.wallet_sendCalls // [!code focus] type Encoded = Rpc.wallet_sendCalls.Encoded // [!code focus] type Decoded = Rpc.wallet_sendCalls.Decoded // [!code focus] ``` ## Shared Schemas Building-block Zod schemas reused across the per-method definitions. ### call * **Type:** `z.ZodMiniObject` Single-call shape (`{ to?, data?, value? }`) used inside `wallet_sendCalls` and friends. ```ts twoslash import { Rpc } from 'accounts' const schema = Rpc.call // [!code focus] ``` ### keyAuthorization * **Type:** `z.ZodMiniObject` TIP-20 key authorization payload (`{ address, chainId, expiry, keyId, keyType, limits?, signature }`). ```ts twoslash import { Rpc } from 'accounts' const schema = Rpc.keyAuthorization // [!code focus] ``` ### keyType * **Type:** `z.ZodMiniUnion<['secp256k1', 'p256', 'webAuthn']>` Supported access key types. ```ts twoslash import { Rpc } from 'accounts' const schema = Rpc.keyType // [!code focus] ``` ### log * **Type:** `z.ZodMiniObject` EVM log shape included in transaction receipts. ```ts twoslash import { Rpc } from 'accounts' const schema = Rpc.log // [!code focus] ``` ### receipt * **Type:** `z.ZodMiniObject` Transaction receipt shape returned by [`eth_sendTransactionSync`](/docs/rpc/eth_sendTransactionSync). ```ts twoslash import { Rpc } from 'accounts' const schema = Rpc.receipt // [!code focus] ``` ### signatureEnvelope * **Type:** `z.ZodMiniType` Signature envelope payload (passes through unchanged via `z.custom`). ```ts twoslash import { Rpc } from 'accounts' const schema = Rpc.signatureEnvelope // [!code focus] ``` ### transactionRequest * **Type:** `z.ZodMiniObject` Transaction request shape accepted by `eth_sendTransaction`, `eth_fillTransaction`, and `eth_signTransaction`. ```ts twoslash import { Rpc } from 'accounts' const schema = Rpc.transactionRequest // [!code focus] ``` ### strictParameters * **Type:** `Record` Lookup of stricter parameter schemas (currently `wallet_authorizeAccessKey` and `wallet_connect`) used internally by [`Provider`](/docs/api/provider) to narrow validation beyond the wire-compatible default. ```ts twoslash import { Rpc } from 'accounts' const strict = Rpc.strictParameters // [!code focus] ``` # `Schema` Zod schemas describing every JSON-RPC method the [`Provider`](/docs/api/provider) handles, plus helpers for defining and combining schema items and translating them into [Ox](https://oxlib.sh) and [Viem](https://viem.sh)-compatible RPC schemas. A `Schema` is a `readonly Item[]`. Each `Item` is a `{ method, params, returns }` triple of Zod schemas. The package ships a `Schema.schema` containing every provider-handled method, derived from [`Rpc`](/docs/api/rpc). ## Usage ```ts twoslash import { Schema } from 'accounts' const item = Schema.defineItem({ method: Schema.schema[0].method, params: Schema.schema[0].params, returns: Schema.schema[0].returns, }) ``` ## Functions ### defineItem * **Type:** `(item: item) => item` Defines a single JSON-RPC method schema item, preserving the literal types of `method`, `params`, and `returns`. ```ts twoslash import { Rpc, Schema } from 'accounts' const item = Schema.defineItem({ // [!code focus] method: Rpc.eth_chainId.schema.method, // [!code focus] params: undefined, // [!code focus] returns: Rpc.eth_chainId.schema.returns, // [!code focus] }) // [!code focus] ``` ### from * **Type:** `(schema: schema) => schema` Creates a `Schema` from a tuple of items. The identity function exists to anchor the inferred tuple type. ```ts twoslash import { Rpc, Schema } from 'accounts' const schema = Schema.from([ // [!code focus] Rpc.eth_accounts.schema, // [!code focus] Rpc.eth_chainId.schema, // [!code focus] ]) // [!code focus] ``` ## Properties ### schema * **Type:** `Schema` Every provider-handled JSON-RPC method, derived from [`Rpc`](/docs/api/rpc). This is the canonical schema used by `Provider`. ```ts twoslash import { Schema } from 'accounts' const items = Schema.schema // [!code focus] ``` ### Request * **Type:** `z.ZodMiniType>` Discriminated union (on `method`) of every provider-handled RPC request. Use it to validate incoming `{ method, params }` payloads. ```ts twoslash import { Schema } from 'accounts' const result = Schema.Request.safeParse({ // [!code focus] method: 'eth_chainId', // [!code focus] }) // [!code focus] ``` ### ox * **Type:** `RpcSchema` [Ox](https://oxlib.sh)-compatible RPC schema union for the provider, suitable for `RpcSchema.from(){:js}`-style consumers. ```ts twoslash import { Schema } from 'accounts' const ox = Schema.ox // [!code focus] ``` ### viem * **Type:** `RpcSchema` [Viem](https://viem.sh)-compatible RPC schema tuple for the provider, suitable for typing `Client`s and `Transport`s. ```ts twoslash import { Schema } from 'accounts' const viem = Schema.viem // [!code focus] ``` ## Types ### Item * **Type:** ```ts type Item = { /** Method name as a Zod literal. */ method: z.ZodMiniLiteral /** Parameters schema, or `undefined` if the method takes no params. */ params: z.ZodMiniType | undefined /** Return type schema, or `undefined` if the method returns nothing. */ returns: z.ZodMiniType | undefined } ``` A single JSON-RPC method definition. ### Schema * **Type:** `readonly Item[]` An array of JSON-RPC method definitions. ### Encoded * **Type:** `Encoded` Wire-format type for a schema item — raw JSON-RPC `{ method, params, returns }` before any codec transforms are applied (hex strings, etc.). ### Decoded * **Type:** `Decoded` Decoded type for a schema item — the runtime shape returned after codec transforms are applied. ### ToOx * **Type:** `ToOx` Transforms a `Schema` into an Ox-compatible `RpcSchema.Generic` union over the wire/encoded form. ### ToViem * **Type:** `ToViem` Transforms a `Schema` into a Viem-compatible `RpcSchema` tuple over the wire/encoded form. # `TrustedHosts` Built-in mappings of dialog hosts (e.g. `tempo.xyz`) to the third-party origins they trust to embed them, plus helpers to test a hostname against a trusted-host list. The `dialog` adapter consults these mappings to decide whether the calling page is allowed to embed the dialog. Patterns starting with `*.` match any subdomain suffix (e.g. `*.workers.dev` matches `foo.workers.dev`). ## Usage ```ts twoslash import { TrustedHosts } from 'accounts' const trusted = TrustedHosts.hosts['tempo.xyz']! const ok = TrustedHosts.match(trusted, 'app.tempo.xyz') ``` ## Properties ### hosts * **Type:** `Record` Trusted host mappings. Each key is a dialog host, and its value is the list of third-party origins that the dialog trusts to embed it. Supports wildcard patterns (e.g. `*.workers.dev`). ```ts twoslash import { TrustedHosts } from 'accounts' const trusted = TrustedHosts.hosts['tempo.xyz'] // [!code focus] ``` ## Functions ### match * **Type:** `(trustedHosts: readonly string[], hostname: string, source?: string) => boolean` Returns `true` if `hostname` matches any pattern in `trustedHosts`, or (when `source` is provided) if `hostname` shares the same registrable domain ("eTLD+1") as `source`. Patterns starting with `*.` match any subdomain suffix (e.g. `*.workers.dev` matches `foo.workers.dev`). ```ts twoslash import { TrustedHosts } from 'accounts' const trusted = TrustedHosts.hosts['tempo.xyz']! const ok = TrustedHosts.match(trusted, 'app.tempo.xyz') // [!code focus] ``` ### sameRegistrableDomain * **Type:** `(a: string, b: string) => boolean` Returns `true` if `a` and `b` share the same registrable domain ("eTLD+1"). ```ts twoslash import { TrustedHosts } from 'accounts' const ok = TrustedHosts.sameRegistrableDomain( // [!code focus] 'app.tempo.xyz', // [!code focus] 'docs.tempo.xyz', // [!code focus] ) // [!code focus] ``` # `WebAuthnCeremony` Pluggable strategy for WebAuthn registration and authentication ceremonies. A `WebAuthnCeremony` controls how challenges are generated and responses are verified during passkey registration and login. # `WebAuthnCeremony.from` Creates a `WebAuthnCeremony` from a custom implementation. ## Usage ```ts import { WebAuthnCeremony } from 'accounts' const ceremony = WebAuthnCeremony.from({ async getRegistrationOptions(params) { /* ... */ }, async verifyRegistration(credential) { /* ... */ }, async getAuthenticationOptions(params) { /* ... */ }, async verifyAuthentication(response) { /* ... */ }, }) ``` ## Properties ### getRegistrationOptions Get credential creation options for `navigator.credentials.create(){:js}`. ```ts declare function getRegistrationOptions(parameters: { /** Credential IDs to exclude (prevents re-registering existing credentials). */ excludeCredentialIds?: readonly string[] | undefined /** Credential display name (e.g. `"alice"`). */ name: string /** Opaque user identifier. Encoded as `user.id` in the WebAuthn creation options. */ userId?: string | undefined }): Promise<{ options: Registration.Options }> ``` ### verifyRegistration Verify a registration response and extract the public key. ```ts declare function verifyRegistration( credential: Registration.Credential, options?: { /** Credential display name forwarded from `getRegistrationOptions`. */ name?: string | undefined } | undefined, ): Promise<{ /** The registered credential's ID. */ credentialId: string /** Email associated with the account. */ email?: string | null | undefined /** The credential's public key (uncompressed P256, hex-encoded). */ publicKey: Hex /** Username associated with the account. */ username?: string | null | undefined }> ``` ### getAuthenticationOptions Get credential request options for `navigator.credentials.get(){:js}`. ```ts declare function getAuthenticationOptions(parameters?: { /** Challenge to use. */ challenge?: Hex | undefined /** Credential ID or IDs to restrict authentication to. */ credentialId?: string | readonly string[] | undefined /** Mediation hint for passkey autofill / conditional UI. */ mediation?: 'conditional' | 'optional' | 'required' | 'silent' | undefined } | undefined): Promise<{ options: Authentication.Options }> ``` ### verifyAuthentication Verify an authentication response and extract the public key. ```ts declare function verifyAuthentication(response: Authentication.Response): Promise<{ /** The authenticated credential's ID. */ credentialId: string /** Email associated with the account. */ email?: string | null | undefined /** The credential's public key (uncompressed P256, hex-encoded). */ publicKey: Hex /** User identifier from the authenticator's `userHandle` (discoverable / conditional flows). */ userId?: string | undefined /** Username associated with the account. */ username?: string | null | undefined }> ``` # `WebAuthnCeremony.server` Creates a server-backed ceremony that delegates to a remote [WebAuthn server handler](/docs/server/handler.webAuthn). All challenge generation, verification, and credential storage happen server-side. The client `fetch()`s four `POST` endpoints derived from the base `url`: * `POST {url}/register/options` — called by `getRegistrationOptions`. * `POST {url}/register` — called by `verifyRegistration`. * `POST {url}/login/options` — called by `getAuthenticationOptions`. * `POST {url}/login` — called by `verifyAuthentication`. ## Usage ```ts import { WebAuthnCeremony } from 'accounts' const ceremony = WebAuthnCeremony.server({ url: 'https://example.com/webauthn' }) ``` ## Parameters ### url * **Type:** `string` * **Required** Base URL of the WebAuthn handler (e.g. `"https://example.com/webauthn"{:js}`). # `Provider` Creates a Provider that bootstraps account access from the terminal via the Wata device-code flow (OAuth 2.0 Device Authorization Grant, RFC 8628 with PKCE). When `wallet_connect` or `wallet_authorizeAccessKey` runs, the provider: * registers a device code at the configured `host` and opens the verification page in the user's default browser * prompts them to approve the access key * persists the resulting key in provider storage for re-use across CLI invocations Subsequent transaction signing happens locally with the managed key — no further browser round-trip is required until the key expires. ## Usage ```ts twoslash [cli.ts] import { Provider } from 'accounts/cli' const provider = Provider.create() ``` ### Authorizing an Access Key The CLI adapter requires `capabilities.authorizeAccessKey` on `wallet_connect` — the user must explicitly grant a scoped access key during the device-code ceremony. ```ts twoslash [cli.ts] // @noErrors import { Expiry } from 'accounts' import { Provider } from 'accounts/cli' import { parseUnits } from 'viem' import { connect } from 'viem/experimental/erc7846' const provider = Provider.create() const client = provider.getClient() await connect(client, { // [!code focus] capabilities: { // [!code focus] authorizeAccessKey: { // [!code focus] expiry: Expiry.days(7), // [!code focus] limits: [{ // [!code focus] token: '0x20c0000000000000000000000000000000000001', // [!code focus] limit: parseUnits('100', 6), // [!code focus] }], // [!code focus] scopes: [{ // [!code focus] address: '0x20c0000000000000000000000000000000000001', // [!code focus] selector: 'transfer(address,uint256)', // [!code focus] }], // [!code focus] }, // [!code focus] }, // [!code focus] }) // [!code focus] ``` ## Parameters ### auth * **Type:** `string | { url?: string; challenge?: string; verify?: string; logout?: string; returnToken?: boolean }` * **Optional** Default Server Authentication configuration for `wallet_connect`. See [`Provider.auth`](/docs/api/provider#auth) for full semantics. ```ts twoslash [cli.ts] import { Provider } from 'accounts/cli' const provider = Provider.create({ auth: '/auth', // [!code focus] }) ``` ### chains * **Type:** `readonly [Chain, ...Chain[]]` * **Default:** `[tempo, tempoModerato, tempoDevnet]` Supported chains. The first chain is the default. ```ts twoslash [cli.ts] import { Provider } from 'accounts/cli' import { tempo } from 'viem/tempo/chains' const provider = Provider.create({ chains: [tempo], // [!code focus] }) ``` ### feePayer * **Type:** `string | false | { url: string; precedence?: 'fee-payer-first' | 'user-first' }` * **Optional** Fee payer configuration. See [`Provider.feePayer`](/docs/api/provider#feepayer) for full semantics. ```ts twoslash [cli.ts] import { Provider } from 'accounts/cli' const provider = Provider.create({ feePayer: 'https://myapp.com/fee-payer', // [!code focus] }) ``` ### host * **Type:** `string` * **Default:** `'https://wallet.tempo.xyz/api/auth/device'` Base URL of the host's device-code endpoints. The CLI registers at `${host}/register`, opens the browser at `${host}/verify?user_code=`, and polls `${host}/token` for completion. Designed to point at a host mounting the [`wata`](https://github.com/wevm/wata) device-code transport. ```ts twoslash [cli.ts] import { Provider } from 'accounts/cli' const provider = Provider.create({ host: 'https://myapp.com/auth/device', // [!code focus] }) ``` ### maxAccounts * **Type:** `number` * **Optional** Maximum number of accounts to persist. Oldest accounts are evicted when exceeded (LRU). ```ts twoslash [cli.ts] import { Provider } from 'accounts/cli' const provider = Provider.create({ maxAccounts: 5, // [!code focus] }) ``` ### mpp * **Type:** `boolean | Provider.mpp.Options` * **Default:** `true` Enable [Machine Payment Protocol](https://mpp.dev) (mppx) support. Enabled by default — pass `false` to disable, or an options object to configure. :::info For CLI tools that need to control the payment mode, use `mpp: { mode: 'pull' }` so the CLI signs transactions locally and forwards the serialized payload to the server. ::: ```ts twoslash [cli.ts] import { Provider } from 'accounts/cli' const provider = Provider.create({ mpp: false, // [!code focus] }) ``` ```ts twoslash [cli.ts] import { Provider } from 'accounts/cli' const provider = Provider.create({ mpp: { // [!code focus] mode: 'pull', // [!code focus] }, // [!code focus] }) ``` ### name * **Type:** `string` * **Default:** `'Tempo CLI'` Display name of the provider. ```ts twoslash [cli.ts] import { Provider } from 'accounts/cli' const provider = Provider.create({ name: 'My CLI', // [!code focus] }) ``` ### open * **Type:** `(url: string, prompt: DeviceCode.Prompt) => Promise | void` * **Default:** `open`/`xdg-open`/`start` depending on platform Browser opener override. Useful for headless environments, CI, or to intercept the URL before opening (e.g. to print it to the terminal). The second argument carries the full pairing prompt (`userCode`, `verificationUri`, expiry, poll interval). ```ts twoslash [cli.ts] import { Provider } from 'accounts/cli' const provider = Provider.create({ open: (url, prompt) => { // [!code focus] console.log(`Open ${url} and confirm code ${prompt.userCode}.`) // [!code focus] }, // [!code focus] }) ``` ### persistCredentials * **Type:** `boolean` * **Default:** `true` Whether to persist credentials and access keys to storage. When `false`, only account addresses are persisted. ```ts twoslash [cli.ts] import { Provider } from 'accounts/cli' const provider = Provider.create({ persistCredentials: false, // [!code focus] }) ``` ### pollingInterval * **Type:** `number` * **Default:** the host-advertised `interval` (5000 when the host omits one) How often (in milliseconds) the CLI polls the device-code token endpoint while waiting for the user to approve the request in the browser. ```ts twoslash [cli.ts] import { Provider } from 'accounts/cli' const provider = Provider.create({ pollingInterval: 1000, // [!code focus] }) ``` ### rdns * **Type:** `string` * **Default:** `'xyz.tempo.cli'` Reverse DNS identifier. ```ts twoslash [cli.ts] import { Provider } from 'accounts/cli' const provider = Provider.create({ rdns: 'com.example.cli', // [!code focus] }) ``` ### relay * **Type:** `string` * **Optional** Base URL for a wallet relay endpoint. See [`Provider.relay`](/docs/api/provider#relay) for full semantics. ```ts twoslash [cli.ts] import { Provider } from 'accounts/cli' const provider = Provider.create({ relay: 'https://myapp.com/relay', // [!code focus] }) ``` ### storage * **Type:** `Storage` * **Default:** `Storage.filesystem({ path: '~/.tempo/wallet/store.json' })` Storage adapter for provider persistence. The CLI default stores connected accounts, active chain state, and managed access keys in one filesystem-backed provider store. ```ts twoslash [cli.ts] import { Provider, Storage } from 'accounts/cli' const provider = Provider.create({ storage: Storage.filesystem({ // [!code focus] path: '~/.config/myapp/tempo-store.json', // [!code focus] }), // [!code focus] }) ``` ### testnet * **Type:** `boolean` * **Default:** `false` Use testnet. When `true`, the default chain will be the first testnet chain in `chains`. ```ts twoslash [cli.ts] import { Provider } from 'accounts/cli' const provider = Provider.create({ testnet: true, // [!code focus] }) ``` ### timeout * **Type:** `number` * **Default:** `300000` (5 minutes) How long (in milliseconds) the CLI waits for the user to complete the device-code ceremony before throwing a timeout error. ```ts twoslash [cli.ts] import { Provider } from 'accounts/cli' const provider = Provider.create({ timeout: 60_000, // [!code focus] }) ``` ### transports * **Type:** `Record` * **Optional** Per-chain transports keyed by chain ID. Layered on top of `relay`. See [`Provider.transports`](/docs/api/provider#transports) for full semantics. ```ts twoslash [cli.ts] import { Provider } from 'accounts/cli' import { http } from 'viem' import { tempo } from 'viem/tempo/chains' const provider = Provider.create({ transports: { // [!code focus] [tempo.id]: http('https://myapp.com/relay/' + tempo.id), // [!code focus] }, // [!code focus] }) ``` # `eth_accounts` Returns the addresses of the accounts currently connected to the provider. Returns an empty array if no accounts are connected. Unlike [`eth_requestAccounts`](/docs/rpc/eth_requestAccounts), this method never prompts the user to connect. ## Request ```ts type Request = { method: 'eth_accounts' } ``` ## Response ```ts type Response = readonly `0x${string}`[] ``` ## Example ```ts const accounts = await provider.request({ method: 'eth_accounts', }) ``` # `eth_chainId` Returns the chain ID of the currently active chain as a hex-encoded number. The active chain can be changed via [`wallet_switchEthereumChain`](/docs/rpc/wallet_switchEthereumChain). ## Request ```ts type Request = { method: 'eth_chainId' } ``` ## Response ```ts type Response = `0x${string}` // chain id (hex-encoded number) ``` ## Example ```ts const chainId = await provider.request({ method: 'eth_chainId', }) ``` # `eth_fillTransaction` Fills missing transaction fields (gas, nonce, fees) via the node, with wallet-aware enrichment — fee token resolution, simulation-based balance diffs, conditional sponsorship, and automatic AMM resolution for insufficient balances. When sent through [`Handler.relay`](/docs/server/handler.relay), the response is enriched with a `capabilities` object containing balance diffs, fee estimates, sponsor info, and swap details. When the sender has a pending locally managed access key, `Provider` attaches the pending `keyAuthorization` before fill so the returned gas covers first-use key publication. Fill and signing-only flows keep that pending authorization available for retry; successful send and broadcast paths consume it. ## Request ```ts type Request = { method: 'eth_fillTransaction' params: [{ /** Access list entries. */ accessList?: { address: `0x${string}`; storageKeys: `0x${string}`[] }[] /** Batch of calls. */ calls?: { /** Calldata. */ data?: `0x${string}` /** Recipient. */ to?: `0x${string}` /** Value to transfer (hex-encoded bigint). */ value?: `0x${string}` }[] /** Chain ID (hex-encoded number). */ chainId?: `0x${string}` /** Top-level calldata (when not using `calls`). */ data?: `0x${string}` /** * Fee payer override. `true` enables the relay's default fee payer, * `false` disables it, or a URL string points at a custom fee payer * service. */ feePayer?: boolean | string /** Pre-computed fee payer signature payload. */ feePayerSignature?: Record /** Fee token to use. If omitted, the relay picks the user's best token. */ feeToken?: `0x${string}` /** Sender address. */ from?: `0x${string}` /** Gas limit (hex-encoded bigint). */ gas?: `0x${string}` /** Key authorization for access key transactions. */ keyAuthorization?: KeyAuthorization /** Raw key material (e.g. compressed/uncompressed public key bytes). */ keyData?: `0x${string}` /** Address of the key being used to sign. */ keyId?: `0x${string}` /** Key type used to sign. */ keyType?: 'secp256k1' | 'p256' | 'webAuthn' /** Max fee per gas (hex-encoded bigint). */ maxFeePerGas?: `0x${string}` /** Max priority fee per gas (hex-encoded bigint). */ maxPriorityFeePerGas?: `0x${string}` /** Nonce (hex-encoded number). */ nonce?: `0x${string}` /** Nonce key (hex-encoded bigint). */ nonceKey?: `0x${string}` /** Top-level recipient (when not using `calls`). */ to?: `0x${string}` /** Value to transfer (hex-encoded bigint). */ value?: `0x${string}` /** Valid after timestamp (hex-encoded number, unix seconds). */ validAfter?: `0x${string}` /** Valid before timestamp (hex-encoded number, unix seconds). */ validBefore?: `0x${string}` }] } ``` ## Response ```ts type Response = { /** Wallet-specific capabilities computed during fill. */ capabilities: { /** AMM swap injected to cover an insufficient balance. */ autoSwap?: { /** Max input amount with slippage applied. */ maxIn: SwapAmount /** Deficit amount that triggered the swap. */ minOut: SwapAmount /** Slippage tolerance (e.g. 0.05 = 5%). */ slippage: number } /** Per-account balance diffs from simulation (swap-related diffs excluded). */ balanceDiffs?: { [account: `0x${string}`]: BalanceDiff[] } /** Fee estimate for the transaction. */ fee?: { /** Raw fee amount in token units. */ amount: `0x${string}` /** Token decimals (e.g. 6). */ decimals: number /** Human-readable fee (e.g. "0.028022"). */ formatted: string /** Token symbol (e.g. "AlphaUSD"). */ symbol: string } /** Structured error details when the fill fails. */ error?: { /** Decoded ABI item that produced the revert (when known). */ abiItem?: Record /** Raw revert data (hex-encoded). */ data?: `0x${string}` /** Revert error name (e.g. "InsufficientBalance"). */ errorName: string /** Human-readable error message. */ message: string } /** Funding requirement when InsufficientBalance is encountered. */ requireFunds?: { /** Deficit amount in token units. */ amount: `0x${string}` /** Token decimals (e.g. 6). */ decimals: number /** Human-readable deficit (e.g. "100.000000"). */ formatted: string /** Token address. */ token: `0x${string}` /** Token symbol (e.g. "USDC.e"). */ symbol: string } /** Sponsor details, present when `sponsored` is `true`. */ sponsor?: { /** Sponsor address. */ address: `0x${string}` /** Sponsor display name. */ name?: string /** Sponsor URL. */ url?: string } /** Whether the transaction is sponsored by a fee payer. */ sponsored: boolean } /** Fully filled transaction. */ tx: Record } type BalanceDiff = { /** Token address. */ address: `0x${string}` /** Token decimals (e.g. 6). */ decimals: number /** Direction relative to the user. */ direction: 'incoming' | 'outgoing' /** Human-readable formatted amount (e.g. "100.00"). */ formatted: string /** Token name (e.g. "USDC.e"). */ name: string /** Addresses receiving this asset. */ recipients: readonly `0x${string}`[] /** Token symbol (e.g. "USDC.e"). */ symbol: string /** Token amount. */ value: `0x${string}` } type SwapAmount = { /** Token decimals. */ decimals: number /** Human-readable formatted amount. */ formatted: string /** Token name (e.g. "AlphaUSD"). */ name: string /** Token symbol (e.g. "AlphaUSD"). */ symbol: string /** Token address. */ token: `0x${string}` /** Amount. */ value: `0x${string}` } ``` ## Example ```ts twoslash // @noErrors import { Provider } from 'accounts' const provider = Provider.create() const [account] = await provider.request({ method: 'eth_accounts', }) // ---cut--- const result = await provider.request({ method: 'eth_fillTransaction', params: [{ from: account, calls: [{ to: '0x20c000000000000000000000b9537d11c60e8b50', // USDC.e // transfer 100 USDC.e data: '0xa9059cbb000000000000000000000000cafebabecafebabecafebabecafebabecafebabe0000000000000000000000000000000000000000000000000000000005f5e100', }], }], }) result.capabilities // { // balanceDiffs: { // '0x1234567890abcdef1234567890abcdef12345678': [{ // address: '0x20c000000000000000000000b9537d11c60e8b50', // decimals: 6, // direction: 'outgoing', // formatted: '100.000000', // name: 'USDC.e', // recipients: ['0xcafebabecafebabecafebabecafebabecafebabe'], // symbol: 'USDC.e', // value: '0x5f5e100', // }], // }, // fee: { // amount: '0x6b86', // decimals: 6, // formatted: '0.027526', // symbol: 'pathUSD', // }, // sponsored: false, // } ``` ### With Fee Token Pay fees with a specific stablecoin. ```ts twoslash // @noErrors import { Provider } from 'accounts' const provider = Provider.create() const [account] = await provider.request({ method: 'eth_accounts' }) // ---cut--- const result = await provider.request({ method: 'eth_fillTransaction', params: [{ from: account, calls: [{ to: '0x20c000000000000000000000b9537d11c60e8b50', // USDC.e // transfer 100 USDC.e data: '0xa9059cbb000000000000000000000000cafebabecafebabecafebabecafebabecafebabe0000000000000000000000000000000000000000000000000000000005f5e100', }], feeToken: '0x20c000000000000000000000b9537d11c60e8b50', // USDC.e }], }) result.capabilities.fee // { // amount: '0x6b86', // decimals: 6, // formatted: '0.027526', // symbol: 'USDC.e', // } ``` ### With Sponsorship When the relay is configured with a [`feePayer`](/docs/server/handler.relay#feepayer), validated transactions are sponsored. The response includes `sponsored: true` and `sponsor` details. ```ts twoslash // @noErrors import { Provider } from 'accounts' const provider = Provider.create() const [account] = await provider.request({ method: 'eth_accounts' }) // ---cut--- const result = await provider.request({ method: 'eth_fillTransaction', params: [{ from: account, calls: [{ to: '0x20c000000000000000000000b9537d11c60e8b50', // USDC.e // transfer 100 USDC.e data: '0xa9059cbb000000000000000000000000cafebabecafebabecafebabecafebabecafebabe0000000000000000000000000000000000000000000000000000000005f5e100', }], }], }) result.capabilities.sponsored // true result.capabilities.sponsor // { // address: '0x1234567890abcdef1234567890abcdef12345678', // name: 'My App', // url: 'https://myapp.com', // } ``` ### AMM Resolution When the user has insufficient balance of a required token, the relay automatically injects swap calls (approve + buy) via the [Stablecoin DEX](https://docs.tempo.xyz/guide/stablecoin-dex). The swap details are reported in `capabilities.autoSwap`, and swap-related balance diffs are excluded from `capabilities.balanceDiffs`. ```ts twoslash // @noErrors import { Provider } from 'accounts' const provider = Provider.create() const [account] = await provider.request({ method: 'eth_accounts' }) // ---cut--- const result = await provider.request({ method: 'eth_fillTransaction', params: [{ from: account, calls: [{ to: '0x20c000000000000000000000b9537d11c60e8b50', // USDC.e // transfer 100 USDC.e data: '0xa9059cbb000000000000000000000000cafebabecafebabecafebabecafebabecafebabe0000000000000000000000000000000000000000000000000000000005f5e100', }], }], }) result.capabilities.balanceDiffs // { // '0x1234567890abcdef1234567890abcdef12345678': [{ // address: '0x20c0000000000000000000000000000000000001', // decimals: 6, // direction: 'outgoing', // formatted: '100.000000', // name: 'AlphaUSD', // recipients: ['0xcafebabecafebabecafebabecafebabecafebabe'], // symbol: 'AlphaUSD', // value: '0x5f5e100', // }], // } result.capabilities.autoSwap // { // maxIn: { // decimals: 6, // formatted: '105.000000', // name: 'AlphaUSD', // symbol: 'AlphaUSD', // token: '0x20c0000000000000000000000000000000000001', // value: '0x6422c40', // }, // minOut: { // decimals: 6, // formatted: '100.000000', // name: 'USDC.e', // symbol: 'USDC.e', // token: '0x20c000000000000000000000b9537d11c60e8b50', // value: '0x5f5e100', // }, // slippage: 0.05, // } result.capabilities.fee // { // amount: '0x6b86', // decimals: 6, // formatted: '0.027526', // symbol: 'pathUSD', // } ``` ### Error Details When `eth_fillTransaction` fails, the relay returns structured error details in `capabilities.error` instead of throwing. ```ts twoslash // @noErrors import { Provider } from 'accounts' const provider = Provider.create() const [account] = await provider.request({ method: 'eth_accounts' }) // ---cut--- const result = await provider.request({ method: 'eth_fillTransaction', params: [{ from: account, calls: [{ to: '0x20c000000000000000000000b9537d11c60e8b50', // USDC.e data: '0xa9059cbb000000000000000000000000cafebabecafebabecafebabecafebabecafebabe0000000000000000000000000000000000000000000000000000000005f5e100', }], }], }) result.capabilities.error // { // errorName: 'BelowMinimumOrderSize', // message: 'Below minimum order size: 1000000.', // } ``` ### Require Funds For `InsufficientBalance` errors, the relay also returns a `capabilities.requireFunds` object with the exact deficit amount and token metadata. ```ts twoslash // @noErrors import { Provider } from 'accounts' const provider = Provider.create() const [account] = await provider.request({ method: 'eth_accounts' }) // ---cut--- const result = await provider.request({ method: 'eth_fillTransaction', params: [{ from: account, calls: [{ to: '0x20c000000000000000000000b9537d11c60e8b50', // USDC.e // transfer 100 USDC.e (but account has 0) data: '0xa9059cbb000000000000000000000000cafebabecafebabecafebabecafebabecafebabe0000000000000000000000000000000000000000000000000000000005f5e100', }], }], }) result.capabilities.requireFunds // { // amount: '0x5f5e100', // decimals: 6, // formatted: '100.000000', // token: '0x20c000000000000000000000b9537d11c60e8b50', // symbol: 'USDC.e', // } result.capabilities.balanceDiffs // { // '0x1234567890abcdef1234567890abcdef12345678': [{ // address: '0x20c000000000000000000000b9537d11c60e8b50', // decimals: 6, // direction: 'outgoing', // formatted: '100.000000', // name: 'USDC.e', // recipients: ['0xcafebabecafebabecafebabecafebabecafebabe'], // symbol: 'USDC.e', // value: '0x5f5e100', // }], // } ``` # `eth_requestAccounts` Returns the addresses of the connected accounts. If no accounts are connected, prompts the user to connect via the wallet's connection flow (equivalent to calling [`wallet_connect`](/docs/rpc/wallet_connect) under the hood) and returns the resulting addresses. Unlike [`eth_accounts`](/docs/rpc/eth_accounts), this method may surface a UI to the user. Prefer [`wallet_connect`](/docs/rpc/wallet_connect) when you need to negotiate capabilities (such as authorizing an access key or running SIWE auth) at the same time. ## Request ```ts type Request = { method: 'eth_requestAccounts' } ``` ## Response ```ts type Response = readonly `0x${string}`[] ``` ## Example ```ts const accounts = await provider.request({ method: 'eth_requestAccounts', }) ``` # `eth_sendTransaction` Signs and broadcasts a transaction, returning the transaction hash. ## Request ```ts type Request = { method: 'eth_sendTransaction' params: [TransactionRequest] } type TransactionRequest = { /** Access list entries. */ accessList?: { address: `0x${string}`; storageKeys: `0x${string}`[] }[] /** Batch of calls. */ calls?: { data?: `0x${string}`; to?: `0x${string}`; value?: `0x${string}` }[] /** Chain ID (hex-encoded number). */ chainId?: `0x${string}` /** Top-level calldata. */ data?: `0x${string}` /** * Fee payer override. `true` enables the relay default, `false` disables * it, or a URL string points at a custom fee payer service. */ feePayer?: boolean | string /** Pre-computed fee payer signature payload. */ feePayerSignature?: Record /** Fee token to use. If omitted, the relay picks the user's best token. */ feeToken?: `0x${string}` /** Sender address. */ from?: `0x${string}` /** Gas limit (hex-encoded bigint). */ gas?: `0x${string}` /** Key authorization for access key transactions. */ keyAuthorization?: KeyAuthorization /** Raw key material. */ keyData?: `0x${string}` /** Address of the key being used to sign. */ keyId?: `0x${string}` /** Key type used to sign. */ keyType?: 'secp256k1' | 'p256' | 'webAuthn' /** Max fee per gas (hex-encoded bigint). */ maxFeePerGas?: `0x${string}` /** Max priority fee per gas (hex-encoded bigint). */ maxPriorityFeePerGas?: `0x${string}` /** Nonce (hex-encoded number). */ nonce?: `0x${string}` /** Nonce key (hex-encoded bigint). */ nonceKey?: `0x${string}` /** Top-level recipient. */ to?: `0x${string}` /** Value to transfer (hex-encoded bigint). */ value?: `0x${string}` /** Valid after timestamp (hex-encoded number, unix seconds). */ validAfter?: `0x${string}` /** Valid before timestamp (hex-encoded number, unix seconds). */ validBefore?: `0x${string}` } ``` ## Response ```ts type Response = `0x${string}` // transaction hash ``` ## Example ```ts const hash = await provider.request({ method: 'eth_sendTransaction', params: [{ to: '0x...', value: '0x0', data: '0x...', }], }) ``` # `eth_sendTransactionSync` Signs, broadcasts, and waits for a transaction receipt. Returns the full receipt instead of just the hash. ## Request ```ts type Request = { method: 'eth_sendTransactionSync' params: [TransactionRequest] } type TransactionRequest = { /** Access list entries. */ accessList?: { address: `0x${string}`; storageKeys: `0x${string}`[] }[] /** Batch of calls. */ calls?: { data?: `0x${string}`; to?: `0x${string}`; value?: `0x${string}` }[] /** Chain ID (hex-encoded number). */ chainId?: `0x${string}` /** Top-level calldata. */ data?: `0x${string}` /** * Fee payer override. `true` enables the relay default, `false` disables * it, or a URL string points at a custom fee payer service. */ feePayer?: boolean | string /** Pre-computed fee payer signature payload. */ feePayerSignature?: Record /** Fee token to use. If omitted, the relay picks the user's best token. */ feeToken?: `0x${string}` /** Sender address. */ from?: `0x${string}` /** Gas limit (hex-encoded bigint). */ gas?: `0x${string}` /** Key authorization for access key transactions. */ keyAuthorization?: KeyAuthorization /** Raw key material. */ keyData?: `0x${string}` /** Address of the key being used to sign. */ keyId?: `0x${string}` /** Key type used to sign. */ keyType?: 'secp256k1' | 'p256' | 'webAuthn' /** Max fee per gas (hex-encoded bigint). */ maxFeePerGas?: `0x${string}` /** Max priority fee per gas (hex-encoded bigint). */ maxPriorityFeePerGas?: `0x${string}` /** Nonce (hex-encoded number). */ nonce?: `0x${string}` /** Nonce key (hex-encoded bigint). */ nonceKey?: `0x${string}` /** Top-level recipient. */ to?: `0x${string}` /** Value to transfer (hex-encoded bigint). */ value?: `0x${string}` /** Valid after timestamp (hex-encoded number, unix seconds). */ validAfter?: `0x${string}` /** Valid before timestamp (hex-encoded number, unix seconds). */ validBefore?: `0x${string}` } ``` ## Response ```ts type Response = TransactionReceipt type TransactionReceipt = { /** Blob base fee per gas (post-EIP-4844). */ blobGasPrice?: bigint /** Total blob gas used by the transaction. */ blobGasUsed?: bigint /** Block hash containing the transaction. */ blockHash: `0x${string}` /** Block number containing the transaction. */ blockNumber: bigint /** Deployed contract address (when this is a contract creation). */ contractAddress: `0x${string}` | null /** Cumulative gas used in the block up to and including this transaction. */ cumulativeGasUsed: bigint /** Effective gas price paid. */ effectiveGasPrice: bigint /** Address that paid the fees on the user's behalf, if any. */ feePayer?: `0x${string}` /** Token used to pay fees, if any. */ feeToken?: `0x${string}` /** Sender address. */ from: `0x${string}` /** Gas used by this transaction. */ gasUsed: bigint /** Logs emitted by the transaction. */ logs: { address: `0x${string}` blockHash: `0x${string}` blockNumber: bigint data: `0x${string}` logIndex: number removed: boolean topics: readonly `0x${string}`[] transactionHash: `0x${string}` transactionIndex: number }[] /** Bloom filter for the receipt logs. */ logsBloom: `0x${string}` /** State root after the transaction (pre-Byzantium receipts). */ root?: `0x${string}` /** Transaction status. */ status: 'success' | 'reverted' /** Recipient address. */ to: `0x${string}` | null /** Transaction hash. */ transactionHash: `0x${string}` /** Index of the transaction within the block. */ transactionIndex: number /** EIP-2718 transaction type (hex-encoded). */ type: `0x${string}` } ``` ## Example ```ts const receipt = await provider.request({ method: 'eth_sendTransactionSync', params: [{ to: '0x...', value: '0x0', data: '0x...', }], }) ``` # `eth_signTransaction` Signs a transaction with the connected account's key and returns the signed transaction as a hex-encoded RLP payload. The returned payload can be broadcast later via `eth_sendRawTransaction`. To sign and broadcast in a single step, use [`eth_sendTransaction`](/docs/rpc/eth_sendTransaction) or [`eth_sendTransactionSync`](/docs/rpc/eth_sendTransactionSync). To preview the filled-in transaction (gas, fees, balance diffs) without signing, use [`eth_fillTransaction`](/docs/rpc/eth_fillTransaction). ## Request ```ts type Request = { method: 'eth_signTransaction' params: [TransactionRequest] } type TransactionRequest = { /** Access list entries. */ accessList?: { address: `0x${string}`; storageKeys: `0x${string}`[] }[] /** Batch of calls. */ calls?: { data?: `0x${string}`; to?: `0x${string}`; value?: `0x${string}` }[] /** Chain ID (hex-encoded number). */ chainId?: `0x${string}` /** Top-level calldata. */ data?: `0x${string}` /** * Fee payer override. `true` enables the relay default, `false` disables * it, or a URL string points at a custom fee payer service. */ feePayer?: boolean | string /** Pre-computed fee payer signature payload. */ feePayerSignature?: Record /** Fee token to use. If omitted, the relay picks the user's best token. */ feeToken?: `0x${string}` /** Sender address. */ from?: `0x${string}` /** Gas limit (hex-encoded bigint). */ gas?: `0x${string}` /** Key authorization for access key transactions. */ keyAuthorization?: KeyAuthorization /** Raw key material. */ keyData?: `0x${string}` /** Address of the key being used to sign. */ keyId?: `0x${string}` /** Key type used to sign. */ keyType?: 'secp256k1' | 'p256' | 'webAuthn' /** Max fee per gas (hex-encoded bigint). */ maxFeePerGas?: `0x${string}` /** Max priority fee per gas (hex-encoded bigint). */ maxPriorityFeePerGas?: `0x${string}` /** Nonce (hex-encoded number). */ nonce?: `0x${string}` /** Nonce key (hex-encoded bigint). */ nonceKey?: `0x${string}` /** Top-level recipient. */ to?: `0x${string}` /** Value to transfer (hex-encoded bigint). */ value?: `0x${string}` /** Valid after timestamp (hex-encoded number, unix seconds). */ validAfter?: `0x${string}` /** Valid before timestamp (hex-encoded number, unix seconds). */ validBefore?: `0x${string}` } ``` ## Response ```ts type Response = `0x${string}` // signed transaction (RLP-encoded) ``` ## Example ```ts const signed = await provider.request({ method: 'eth_signTransaction', params: [{ to: '0x...', value: '0x0', data: '0x...', }], }) ``` # `eth_signTypedData_v4` Signs an [EIP-712](https://eips.ethereum.org/EIPS/eip-712) typed data structure with the connected account's key. The typed data is supplied as a JSON-stringified payload following the v4 format. To sign an arbitrary message instead of structured data, use [`personal_sign`](/docs/rpc/personal_sign). ## Request ```ts type Request = { method: 'eth_signTypedData_v4' params: [ `0x${string}`, // signer address string, // JSON-stringified EIP-712 typed data payload ] } ``` The typed data payload is a JSON string with the following shape: ```ts type TypedData = { domain: { name?: string version?: string chainId?: number verifyingContract?: `0x${string}` salt?: `0x${string}` } types: Record primaryType: string message: Record } ``` ## Response ```ts type Response = `0x${string}` // signature ``` ## Example ```ts const signature = await provider.request({ method: 'eth_signTypedData_v4', params: [ '0x...', JSON.stringify({ domain: { name: 'Example', version: '1', chainId: 1, verifyingContract: '0x...', }, types: { EIP712Domain: [ { name: 'name', type: 'string' }, { name: 'version', type: 'string' }, { name: 'chainId', type: 'uint256' }, { name: 'verifyingContract', type: 'address' }, ], Mail: [ { name: 'from', type: 'address' }, { name: 'to', type: 'address' }, { name: 'contents', type: 'string' }, ], }, primaryType: 'Mail', message: { from: '0x...', to: '0x...', contents: 'Hello, Tempo!', }, }), ], }) ``` # `personal_sign` Signs an arbitrary message with the connected account's key. ## Request ```ts type Request = { method: 'personal_sign' params: [ Hex, // message Address // signer address ] } ``` ## Response ```ts type Response = Hex // signature ``` ## Example ```ts import { Hex } from 'ox' const signature = await provider.request({ method: 'personal_sign', params: [ Hex.fromString('Hello, Tempo!'), '0x...', ], }) ``` # `wallet_authorizeAccessKey` Authorizes an access key with an expiry, spending limits, and optional call scopes for delegated transaction signing. ## Request ```ts type Request = { method: 'wallet_authorizeAccessKey' params: [{ /** Address of the key (alternative to `publicKey`). */ address?: Address /** * Chain ID to scope the authorization to. * Hex-encoded (`0x...`) on the wire, decoded to `bigint`. */ chainId?: bigint /** Unix timestamp (seconds) when the key expires. */ expiry: number /** Key type. */ keyType?: 'secp256k1' | 'p256' | 'webAuthn' /** TIP-20 spending limits. */ limits?: { /** Token contract address. */ token: Address /** Spending cap. Hex-encoded (`0x...`) on the wire, decoded to `bigint`. */ limit: bigint /** Window length in seconds. Omit for an absolute (non-resetting) cap. */ period?: number }[] /** Public key to authorize. */ publicKey?: Hex /** Per-target call scopes. The key may only call these targets. */ scopes?: { /** Target contract address the key may call. */ address: Address /** Function selector (4-byte `0x...`) or human-readable signature. */ selector?: Hex | string /** Allowed call recipients (e.g. `transfer(to, ...)`). */ recipients?: Address[] }[] /** Optional funding prompt to show after approval. */ showDeposit?: ShowDeposit }] } type ShowDeposit = boolean | { /** Funding path to open. Use mach for MACH checkout. */ intent?: 'applePay' | 'credits' | 'crypto' | 'faucet' | 'mach' | 'referralCode' | 'x' /** Human-readable amount to pre-fill (e.g. `"50"`). */ amount?: string /** Display name shown in the deposit UI. */ displayName?: string /** Token symbol or contract address to pre-fill. */ token?: string | Address } ``` Tempo Wallet-hosted approval pages require at least one `limits` entry and one `scopes` entry. Use the `address` field on each scope for the target contract; malformed scope entries are rejected before approval. Set `showDeposit` to `true` or funding hints to show the optional deposit picker before the wallet returns the authorization result. ## Response ```ts type Response = { /** The signed authorization payload. */ keyAuthorization: KeyAuthorization /** Root account address that issued the authorization. */ rootAddress: Address } type KeyAuthorization = { /** Address of the access key. */ address: Address /** Chain ID the authorization is scoped to. Hex on the wire. */ chainId: bigint /** Unix timestamp (seconds) when the authorization expires. May be `null`. */ expiry: number | null | undefined /** Address derived from the key. */ keyId: Address /** Key type. */ keyType: 'secp256k1' | 'p256' | 'webAuthn' /** Signed spending limits, if any. */ limits?: readonly { token: Address limit: bigint period?: number }[] /** Signature envelope from the root account. */ signature: SignatureEnvelope } ``` ## Example ```ts import { Expiry } from 'accounts' const result = await provider.request({ method: 'wallet_authorizeAccessKey', params: [ { expiry: Expiry.days(1), limits: [ { token: '0x20c0000000000000000000000000000000000001', limit: '0x5F5E100' }, ], scopes: [{ address: '0x20c0000000000000000000000000000000000001' }], showDeposit: { amount: '50', token: 'USDC' }, }, ], }) ``` # `wallet_connect` Requests to connect account(s) with optional capabilities. Set `capabilities.showDeposit` to `true` or funding hints to show an optional deposit prompt after an account connects. The deposit target is always the connected account on the active connect chain. ## Request `capabilities` is a discriminated union over `method` — `'register'` or `'login'`. Both branches share the `digest`, `authorizeAccessKey`, `auth`, `identity`, `personalSign`, and `showDeposit` fields. ```ts type Request = { method: 'wallet_connect' params?: [{ capabilities?: ConnectRegisterCapabilities | ConnectLoginCapabilities /** Chain ID to connect to (hex-encoded number on the wire). */ chainId?: number /** Caller-supplied protocol version string. */ version?: string }] } type ConnectRegisterCapabilities = { method: 'register' /** Optional digest to bind into the registration ceremony. */ digest?: `0x${string}` /** Authorize an access key during the same ceremony. */ authorizeAccessKey?: AuthorizeAccessKeyCapability /** SIWE round-trip configuration. See `Auth` below. */ auth?: Auth /** Request consented identity claims (e.g. a verified email). See `Identity` below. */ identity?: Identity /** Display name to associate with the new account. */ name?: string /** Sign an EIP-191 message in the same passkey ceremony. */ personalSign?: { message: string } /** Show an optional deposit prompt after the account connects. */ showDeposit?: ShowDeposit /** Stable user identifier passed to the WebAuthn credential. */ userId?: string } type ConnectLoginCapabilities = { method?: 'login' /** Optional digest to bind into the login ceremony. */ digest?: `0x${string}` /** WebAuthn credential ID or IDs to authenticate with. */ credentialId?: string | string[] /** Authorize an access key during the same ceremony. */ authorizeAccessKey?: AuthorizeAccessKeyCapability /** SIWE round-trip configuration. See `Auth` below. */ auth?: Auth /** Request consented identity claims (e.g. a verified email). See `Identity` below. */ identity?: Identity /** Sign an EIP-191 message in the same passkey ceremony. */ personalSign?: { message: string } /** Show an optional deposit prompt after the account connects. */ showDeposit?: ShowDeposit /** Show an account picker before authenticating. */ selectAccount?: boolean } type Identity = { /** * Request the account's verified email. Returned on the connected account * as `capabilities.identity.email` (a plain string, for client-side use) * and `capabilities.identity.idToken` (a signed token, for server-side * verification). * * - `true` — request the email; the wallet auto-issues the signed token, * reusing the `auth` (SIWE) challenge nonce when present. * - `'user@example.com'` — require this exact email: the wallet forces the * user to authenticate with the account owning it (verifying it first * when needed) instead of a free-form entry. * - `{ address, nonce }` — same as above, with an explicit nonce baked into * the token (for standalone use without the `auth` capability). Both * fields are optional; `{ nonce }` alone requests any email. * - `{ domains }` — restrict email entry to these domains: the wallet * rejects entering an email outside them. An account's already-verified * email is still shared; verify server-side. */ email?: boolean | string | { address?: string; domains?: string[]; nonce?: string } } type ShowDeposit = boolean | { /** Funding path to open. Use mach for MACH checkout. */ intent?: 'applePay' | 'credits' | 'crypto' | 'faucet' | 'mach' | 'referralCode' | 'x' /** Human-readable amount to pre-fill (e.g. `"50"`). */ amount?: string /** Display name shown in the deposit UI (e.g. the app name). */ displayName?: string /** Connect method that should show the deposit prompt. Defaults to both methods. */ on?: 'login' | 'register' /** Token contract address or supported token symbol to pre-fill. */ token?: `0x${string}` | string } type AuthorizeAccessKeyCapability = { address?: Address /** Hex on the wire, decoded to `bigint`. */ chainId?: bigint expiry: number keyType?: 'secp256k1' | 'p256' | 'webAuthn' limits?: { token: Address /** Hex on the wire, decoded to `bigint`. */ limit: bigint /** Window length in seconds. Omit for an absolute cap. */ period?: number }[] publicKey?: Hex scopes?: { address: Address selector?: Hex | string recipients?: Address[] }[] } type Auth = string | { /** * Base URL. The SDK derives `${url}/challenge` and `${url}/logout`; * `${url}` itself is the verify endpoint. */ url?: string /** Override individual endpoints. Either `url` or both `challenge` + `verify` must be set. */ challenge?: string verify?: string logout?: string /** * Ask the verify endpoint to also return `{ token }` in the JSON body. * Default `false` — cookie mode relies on `Set-Cookie` only. */ returnToken?: boolean } ``` ## Response ```ts type Response = { accounts: { address: Address capabilities: { /** * Consented identity claims, present only when requested via * `capabilities.identity` AND approved by the user. Omitted entirely * otherwise. Only set on the authenticated account. */ identity?: { /** Verified email address, if requested and approved. May be `null`. */ email?: string | null /** * Signed identity token (JWT) asserting the verified email, bound to * the requesting app origin (`aud`) and the connected account (`sub`). * Verify it server-side against the wallet's JWKS. Present only when * `identity.email` was requested and a verified email is available. */ idToken?: string } /** Authorized access key, if `authorizeAccessKey` was requested. */ keyAuthorization?: KeyAuthorization /** * Echo of the `personalSign` request, present iff the caller set * `capabilities.personalSign` (or `capabilities.auth` folded a SIWE * message into the same slot). The signature itself lives on * `capabilities.signature`. */ personalSign?: { message: string } /** Signature for `personalSign` / SIWE message, if requested. */ signature?: Hex /** Authenticated username, if available. May be `null`. */ username?: string | null /** * SIWE round-trip output, populated when the request `auth` * capability was set. `token` is present in JWT mode or when the * request set `returnToken: true`. Cookie-mode default = `{}`. */ auth?: { token?: string } } }[] } ``` ## Example ```ts import { Provider } from 'accounts' const provider = Provider.create() const { accounts } = await provider.request({ method: 'wallet_connect', }) ``` ### Requesting a verified email Set `capabilities.identity.email` to `true` to ask for the account's verified email. The wallet runs an in-flow approval after the connect ceremony: if the account already has a verified email it shows a consent prompt; otherwise it collects and verifies an email (one-time code) before sharing it. The user can decline — `wallet_connect` still succeeds, just without `identity`. ```ts const { accounts } = await provider.request({ method: 'wallet_connect', params: [{ capabilities: { identity: { email: true } } }], }) // Present only if the user approved sharing; set on the authenticated account. const email = accounts[0]?.capabilities?.identity?.email ``` The response also carries a signed `idToken` alongside the plain `email`. The plain string is convenient for client-side display, but it can be set by anyone. Forward the `idToken` to your backend and verify it against the wallet's JWKS to trust the email server-side. ```ts const identity = accounts[0]?.capabilities?.identity // `email` for display; `idToken` to prove the email to your backend. const { email, idToken } = identity ?? {} ``` The token is a standard OIDC ID token (JWT) bound to your app origin (`aud`) and the connected account address (`sub`). Verify it with any OIDC-aware library, or with [`Identity.verify`](/docs/server/identity.verify). ### Requiring a specific email Set `capabilities.identity.email` to an email address to require that exact email. The wallet pre-fills and locks the sign-in entry to it: the user must authenticate with the account that owns the email (verifying it with a one-time code first when needed) and cannot substitute another one. Connecting an account whose verified email differs is rejected. Use this when your app already knows who is signing in — an invite link, a pre-auth session, or a server-side allowlist. ```ts const { accounts } = await provider.request({ method: 'wallet_connect', params: [{ capabilities: { identity: { email: 'user@example.com' } } }], }) ``` Use the object form to also bind an explicit nonce into the issued token: ```ts const { accounts } = await provider.request({ method: 'wallet_connect', params: [ { capabilities: { identity: { email: { address: 'user@example.com', nonce } }, }, }, ], }) ``` ### Restricting email domains Set `capabilities.identity.email` to `{ domains }` to restrict email entry to an allowlist of domains. The wallet rejects entering an email outside them (matched case-insensitively) in both the sign-in entry and the in-flow email prompt. Use this when your server restricts sign-ups to approved domains, so users get immediate feedback instead of a rejection after authenticating. ```ts const { accounts } = await provider.request({ method: 'wallet_connect', params: [ { capabilities: { identity: { email: { domains: ['tempo.xyz'] } }, }, }, ], }) ``` The restriction applies to email *entry* only: an account whose email is already verified still shares it, since your server may accept existing or invited users beyond the allowlist. Always enforce the policy server-side by verifying the `idToken`. An exact `address` takes precedence over `domains`. # `wallet_deposit` Opens the wallet's deposit flow so the user can fund an account. The wallet renders the deposit UI (for example onramp, faucet, or transfer-from-an-external-wallet flows) using any pre-filled hints supplied in the request. The wallet picks the deposit path it supports for the requested token and chain. For paths that move funds onchain (such as a testnet faucet drop), the wallet returns the resulting receipts. For offchain paths like Apple Pay, funds arrive asynchronously and `receipts` is omitted. This method is supported by Tempo Wallet's browser, mobile web authentication, and CLI device-code adapters. Local signing adapters without a deposit UI reject the request as unsupported. ## Request ```ts type Request = { method: 'wallet_deposit' params: [{ /** Recipient address. Defaults to the connected account. */ address?: `0x${string}` /** Chain ID to deposit to. */ chainId?: number /** Display name shown in the deposit UI (e.g. the app name). */ displayName?: string /** Preferred funding path to show first. */ intent?: 'applePay' | 'credits' | 'crypto' | 'faucet' | 'mach' | 'referralCode' | 'x' /** Token symbol or contract address to pre-fill. */ token?: string /** Human-readable amount to pre-fill (e.g. `"1.5"`). */ amount?: string }] } ``` ## Response ```ts type Response = { /** * Receipts of any onchain operations performed during the deposit * (e.g. testnet faucet drops). Absent for offchain paths like Apple * Pay where funds arrive asynchronously. */ receipts?: TransactionReceipt[] } | undefined ``` ## Example Use `Mach.fund` to open MACH checkout for the connected account. The helper selects the mainnet MACH token and validates the amount before requesting checkout. The user reviews and confirms the payment in Tempo Wallet. ```ts import { Mach } from 'accounts' await Mach.fund(provider, { amount: '5' }) ``` For a terminal or another device that cannot display the wallet, generate a link bound to the intended account: ```ts import { Mach } from 'accounts' const url = Mach.getFundingUrl({ address: '0x0000000000000000000000000000000000000001', amount: '5', }) ``` The same helpers are exported from `accounts/cli`. `Mach.fund` uses the CLI provider's device-code browser handoff when called with a CLI provider. The underlying request is also available directly: ```ts await provider.request({ method: 'wallet_deposit', params: [{ intent: 'mach', token: Mach.token.address, chainId: '0x1079', amount: '5', }], }) ``` MACH checkout currently supports Tempo mainnet (4217). Amounts are USD decimal strings with at most two decimal places; the checkout applies its current purchase limits. The deprecated `credits` intent remains accepted for older clients and opens MACH checkout in updated wallets. Existing MPP Credits remain a separate balance and are not converted by opening checkout. Opening a link, closing the wallet, or receiving an empty response does not confirm a purchase. Check the resulting MACH balance or confirmed funding receipt before spending. # `wallet_depositZone` Opens the wallet's zone-deposit flow so the user can move funds from their account into a zone. The wallet renders the deposit UI using any pre-filled hints supplied in the request. To withdraw funds back out of a zone, use [`wallet_withdrawZone`](/docs/rpc/wallet_withdrawZone). This method is only supported by adapters that opt in to the `depositZone` action (typically the [`dialog`](/docs/api/dialog) adapter). Other adapters reject the request as an unsupported method. ## Request ```ts type Request = { method: 'wallet_depositZone' params?: [{ /** Human-readable amount to pre-fill (e.g. `"1.5"`). */ amount?: string /** * Fee payer override. `false` disables the wallet's default fee * payer, a URL string points at a custom fee payer service. */ feePayer?: boolean | string /** Token contract address to pre-fill. Omit to let the user choose. */ token?: `0x${string}` /** Zone id to deposit into. */ zoneId?: number }] } ``` ## Response ```ts type Response = { /** Receipt of the submitted deposit. */ receipt: TransactionReceipt } ``` ## Example ```ts const { receipt } = await provider.request({ method: 'wallet_depositZone', params: [{ zoneId: 1, token: '0x20c0000000000000000000000000000000000001', amount: '10', }], }) ``` # `wallet_disconnect` Disconnects the connected account(s) and clears session state. ## Request ```ts type Request = { method: 'wallet_disconnect' } ``` ## Response ```ts type Response = undefined ``` ## Example ```ts await provider.request({ method: 'wallet_disconnect', }) ``` # `wallet_getBalances` Returns token balances for the connected account. ## Request ```ts type Request = { method: 'wallet_getBalances' params?: [{ /** Account address. Defaults to connected account. */ account?: Address /** Chain ID. */ chainId?: number /** Filter to specific token addresses. */ tokens?: Address[] }] } ``` ## Response ```ts type Response = { address: Address balance: bigint decimals: number display: string name: string symbol: string }[] ``` ## Example ```ts const balances = await provider.request({ method: 'wallet_getBalances', }) ``` # `wallet_getCallsStatus` Returns the status and receipts of a previously submitted batch of calls. ## Request ```ts type Request = { method: 'wallet_getCallsStatus' /** Bundle id returned from `wallet_sendCalls` (a plain string, not necessarily hex). */ params?: [string] } ``` ## Response ```ts type Response = { /** Whether the bundle executed atomically. */ atomic: boolean /** Chain ID the bundle was submitted to. */ chainId: number /** Bundle id (echo of the request id). */ id: string /** Per-call receipts, present once the bundle has been mined. */ receipts?: TransactionReceipt[] /** EIP-5792 numeric status code. */ status: number /** Protocol version string. */ version: string } ``` ## Example ```ts const status = await provider.request({ method: 'wallet_getCallsStatus', params: ['0x9a8b7c...'], }) ``` # `wallet_getCapabilities` Returns capabilities for the connected account across chains. ## Request ```ts type Request = { method: 'wallet_getCapabilities' params?: [Address] | [Address, Hex[]] } ``` ## Response ```ts type Response = Record ``` ## Example ```ts const capabilities = await provider.request({ method: 'wallet_getCapabilities', params: ['0x...'], }) ``` # `wallet_revokeAccessKey` Revokes a previously authorized access key. ## Request ```ts type Request = { method: 'wallet_revokeAccessKey' params: [{ /** Address of the account. */ address: Address /** Address of the access key to revoke. */ accessKeyAddress: Address /** * Fee sponsorship for the revocation transaction. `true` uses the * provider or remote wallet's configured sponsor, `false` opts out, * and a URL requests sponsorship from that endpoint. */ feePayer?: boolean | string }] } ``` ## Response ```ts type Response = undefined ``` ## Example ```ts await provider.request({ method: 'wallet_revokeAccessKey', params: [{ address: '0x...', accessKeyAddress: '0x...', feePayer: 'https://example.com/fee-payer', }], }) ``` # `wallet_send` Opens the wallet's send-token flow so the user can transfer a token to another address. The wallet renders the send UI using any pre-filled hints supplied in the request, then signs and broadcasts the transfer once the user confirms. To send raw transactions or batches without surfacing the wallet's send UI, use [`eth_sendTransaction`](/docs/rpc/eth_sendTransaction) or [`wallet_sendCalls`](/docs/rpc/wallet_sendCalls) instead. This method is only supported by adapters that opt in to the `send` action (typically the [`dialog`](/docs/api/dialog) adapter). Other adapters reject the request as an unsupported method. ## Request ```ts type Request = { method: 'wallet_send' params?: [{ /** Human-readable amount to pre-fill (e.g. `"1.5"`). */ amount?: string /** * Fee payer override. `false` disables the wallet's default fee * payer, a URL string points at a custom fee payer service. */ feePayer?: boolean | string /** * UTF-8 memo (max 32 bytes) to attach to the transfer. The wallet * rejects the request if the selected token does not support * memos (non-TIP-20). */ memo?: string /** Recipient address to pre-fill. */ to?: `0x${string}` /** * Token to pre-fill, accepted as either a contract address or a * curated tokenlist symbol (case-insensitive, e.g. `"pathUsd"`). * Symbols are resolved against the curated tokenlist on the * active chain. Omit to let the user choose. */ token?: `0x${string}` | string }] } ``` ## Response ```ts type Response = { /** Chain ID the send was submitted to. */ chainId: number /** Receipt of the submitted send. */ receipt: TransactionReceipt } ``` ## Example ```ts const { receipt } = await provider.request({ method: 'wallet_send', params: [{ amount: '1.5', memo: 'invoice #4821', to: '0x...', token: 'pathUsd', }], }) ``` # `wallet_sendCalls` Sends a batch of calls from the connected account. Supports atomic execution. ## Request ```ts type Request = { method: 'wallet_sendCalls' params?: [{ /** Require atomic execution; reject if the wallet can't guarantee it. */ atomicRequired?: boolean /** Calls to execute. */ calls: { to?: Address; data?: Hex; value?: Hex }[] capabilities?: { /** * Fee payer override. `true` enables the relay's default fee payer, * `false` disables it, or a URL string points at a custom fee payer * service. */ feePayer?: boolean | string /** Wait for receipts before returning. */ sync?: boolean } /** Chain ID to submit on. */ chainId?: number /** Sender address. */ from?: Address /** Caller-supplied protocol version string. */ version?: string }] } ``` ## Response ```ts type Response = { /** Whether the bundle executed atomically. */ atomic?: boolean /** Capabilities echoed back from the request. */ capabilities?: { feePayer?: boolean | string sync?: boolean } /** Chain ID the bundle was submitted to. */ chainId?: number /** Bundle id (use with `wallet_getCallsStatus`). */ id: string /** Per-call receipts when `sync: true`. */ receipts?: TransactionReceipt[] /** EIP-5792 numeric status code. */ status?: number /** Protocol version string. */ version?: string } ``` ## Example ```ts const result = await provider.request({ method: 'wallet_sendCalls', params: [{ calls: [ { to: '0x...', data: '0x...' }, ], }], }) ``` # `wallet_swap` Opens the wallet's swap flow so the user can swap one token for another. The wallet renders the swap UI using any pre-filled intent supplied in the request, then signs and broadcasts the swap once the user confirms. This method is only supported by adapters that opt in to the `swap` action (typically the [`dialog`](/docs/api/dialog) adapter). Other adapters reject the request as an unsupported method. ## Request ```ts type Request = { method: 'wallet_swap' params?: [{ /** Human-readable amount to pre-fill (e.g. `"1.5"`). */ amount?: string /** * Other side of the swap pair. For buys, this is the token to sell. * For sells, this is the token to buy. */ pairToken?: `0x${string}` /** * Maximum allowed slippage as a decimal fraction * (for example `0.05` for 5%). Must be between `0` and `1`. */ slippage?: number /** Token to buy or sell. Omit to let the user choose. */ token?: `0x${string}` /** * Whether the amount is an exact buy amount (`swapExactAmountOut`) * or sell amount (`swapExactAmountIn`). */ type?: 'buy' | 'sell' }] } ``` ## Response ```ts type Response = { /** Receipt of the submitted swap. */ receipt: TransactionReceipt } ``` ## Example ```ts const { receipt } = await provider.request({ method: 'wallet_swap', params: [{ type: 'sell', token: '0x20c0000000000000000000000000000000000001', pairToken: '0x20c0000000000000000000000000000000000002', amount: '1.5', slippage: 0.05, }], }) ``` # `wallet_switchEthereumChain` Switches the provider's active chain to the chain identified by `chainId`. The chain must be one of the chains the provider was configured with — switching to an unknown chain throws an `UnsupportedChainId` error. After a successful switch, [`eth_chainId`](/docs/rpc/eth_chainId) reflects the new chain and subsequent requests use it as their default. ## Request ```ts type Request = { method: 'wallet_switchEthereumChain' params: [{ /** Chain ID to switch to (hex-encoded number on the wire, decoded to `number`). */ chainId: number }] } ``` ## Response ```ts type Response = undefined ``` ## Example ```ts await provider.request({ method: 'wallet_switchEthereumChain', params: [{ chainId: 1 }], }) ``` # `wallet_withdrawZone` Opens the wallet's zone-withdraw flow so the user can move funds out of a zone back to their account. The wallet renders the withdraw UI using any pre-filled hints supplied in the request. To deposit funds into a zone, use [`wallet_depositZone`](/docs/rpc/wallet_depositZone). This method is only supported by adapters that opt in to the `withdrawZone` action (typically the [`dialog`](/docs/api/dialog) adapter). Other adapters reject the request as an unsupported method. ## Request ```ts type Request = { method: 'wallet_withdrawZone' params?: [{ /** Human-readable amount to pre-fill (e.g. `"1.5"`). */ amount?: string /** Token contract address to pre-fill. Omit to let the user choose. */ token?: `0x${string}` /** Zone id to withdraw from. */ zoneId?: number }] } ``` ## Response ```ts type Response = { /** Receipt of the submitted withdrawal. */ receipt: TransactionReceipt } ``` ## Example ```ts const { receipt } = await provider.request({ method: 'wallet_withdrawZone', params: [{ zoneId: 1, token: '0x20c0000000000000000000000000000000000001', amount: '10', }], }) ``` # `Remote` The `Remote` namespace is the **wallet-side** counterpart to the [`Provider`](/docs/api/provider). It: * runs inside the iframe or popup served by the wallet's auth app * receives RPC requests forwarded from the host SDK over [`Messenger`](https://github.com/tempoxyz/accounts/blob/main/src/core/Messenger.ts) * exposes a queue of pending requests for the UI to render approval surfaces against A typical wallet app calls [`Remote.create`](/docs/api/remote.create) once at startup, subscribes to the request queue with `onUserRequest`, navigates to a per-method route, and either [`respond`](#respond) or [`reject`](#reject)s once the user has approved or cancelled. ## Core ## React The React hooks live in the `accounts/react` entrypoint and are designed to be used from inside the wallet's auth app. # `Remote.create` Creates a remote context for the wallet's iframe/popup app. The context bundles: * a [`Messenger`](https://github.com/tempoxyz/accounts/blob/main/src/core/Messenger.ts) bridge to the host SDK * a [`Provider`](/docs/api/provider) used to actually execute approved requests * a [Zustand](https://github.com/pmndrs/zustand) store of queued RPC requests ## Usage The `messenger` is a [`Messenger.Bridge`](https://github.com/tempoxyz/accounts/blob/main/src/core/Messenger.ts) that connects the wallet app to its host (the iframe `parent` or popup `opener`). The `provider` is the wallet's own [`Provider`](/docs/api/provider) instance -- the one that holds the wallet's accounts and actually executes signing operations. ```ts import { Messenger, Provider, Remote, webAuthn } from 'accounts' // 1. Build a bridge to the parent/opener (the page that opened this iframe/popup). const target = window.opener ?? window.parent const messenger = Messenger.bridge({ from: Messenger.fromWindow(window), to: Messenger.fromWindow(target), }) // 2. Build the wallet-side provider that will execute approved requests. const provider = Provider.create({ adapter: webAuthn({ auth: '/auth' }), }) // 3. Wire them together. const remote = Remote.create({ messenger, provider, }) remote.onUserRequest(({ account, request }) => { if (!request) return // Navigate to the request's approval UI... }) remote.ready() ``` ## Parameters ### messenger * **Type:** `Messenger.Bridge` * **Required** Bridge messenger used to receive RPC requests from the host SDK and to send back responses, sync events, and mode switches. ```ts import { Remote } from 'accounts' const remote = Remote.create({ messenger, // [!code focus] provider, }) ``` ### provider * **Type:** `Provider.Provider` * **Required** [`Provider`](/docs/api/provider) instance used to execute RPC requests once the user approves them. ```ts import { Remote } from 'accounts' const remote = Remote.create({ messenger, provider, // [!code focus] }) ``` ### trustedHosts * **Type:** `readonly string[]` * **Default:** `[]` Hostnames trusted to render the wallet in an iframe. Forwarded to [`Messenger.ready`](https://github.com/tempoxyz/accounts/blob/main/src/core/Messenger.ts) and consulted by [`Remote.useEnsureVisibility`](/docs/api/remote.useEnsureVisibility) to decide whether occlusion monitoring is required. ```ts import { Remote } from 'accounts' const remote = Remote.create({ messenger, provider, trustedHosts: ['example.com', '*.example.com'], // [!code focus] }) ``` ## Returns `Remote.create` returns a `Remote` object with the following properties: ### messenger * **Type:** `Messenger.Bridge` The messenger passed in via options. ### provider * **Type:** `Provider.Provider` The provider passed in via options. ### store * **Type:** `StoreApi` Zustand store holding the remote context's state: ```ts type State = { /** Whether the dialog is rendered in an iframe or popup. */ mode: 'iframe' | 'popup' | undefined /** Trusted host origin from MessageEvent. */ origin: string | undefined /** Whether the dialog is ready to display content. */ ready: boolean /** Queued RPC requests received from the host. */ requests: Remote.State['requests'] } ``` ### trustedHosts * **Type:** `readonly string[]` The trusted hosts list passed in via options (defaulted to `[]`). ### onUserRequest * **Type:** `(cb: (payload: { account; origin; request }) => void | Promise) => () => void` Subscribes to user-facing RPC requests. Syncs the host's active account/chain, updates the remote store, and invokes the callback with the first pending request -- or `null` when the queue is cleared, signalling the UI should close. Returns an unsubscribe function. ```ts import { Remote } from 'accounts' const remote = Remote.create({ messenger, provider }) const unsubscribe = remote.onUserRequest(({ account, request }) => { // [!code focus] if (!request) return // Queue cleared -- close the dialog. // [!code focus] // Navigate to the per-method approval UI. // [!code focus] }) // [!code focus] ``` ### onRequests * **Type:** `(cb: (requests, event, extra) => void) => () => void` Lower-level subscription that fires on every `rpc-requests` message and receives the full queued request list, the underlying `MessageEvent`, and the host's active account. Used internally by `onUserRequest`. Returns an unsubscribe function. ### ready * **Type:** `(options?: { accounts?: readonly string[] } & Messenger.ReadyOptions) => void` Signals readiness to the host and begins accepting requests. Call this after the remote context is fully initialized. When `accounts` is provided, the wallet responds to SDK account-sync messages. ```ts import { Remote } from 'accounts' const remote = Remote.create({ messenger, provider }) remote.ready({ accounts: ['0x...'] }) // [!code focus] ``` ### reject * **Type:** `(request: NonNullable, error?: ProviderRpcError | RpcResponse.BaseError) => void` Rejects a single RPC request. Defaults to `Provider.UserRejectedRequestError` when no `error` is provided. ```ts remote.reject(request) // [!code focus] ``` ### rejectAll * **Type:** `(error?: ProviderRpcError | RpcResponse.BaseError) => void` Rejects every pending RPC request. Typically wired to backdrop clicks and dialog dismissals. ```ts remote.rejectAll() // [!code focus] ``` ### respond * **Type:** `(request, options?: respond.Options) => Promise` Responds to an RPC request. When `options.result` is provided, sends it directly. When `options.error` is provided, sends an error response. Otherwise, executes `provider.request(request)` and sends the result. ```ts declare namespace respond { type Options = { /** Error to respond with (takes precedence over result). */ error?: { code: number; message: string } | undefined /** * Called when `provider.request()` throws. Return `true` to suppress the * error response to the parent -- the dialog stays open and can show a * recovery UI. The error is still re-thrown to the caller. */ onError?: ((error: Error) => boolean | void) | undefined /** Explicit result -- if omitted, calls `provider.request(request)`. */ result?: unknown | undefined /** Transform the result before sending. */ selector?: ((result: any) => unknown) | undefined } } ``` ```ts // Defer to the bound provider. // [!code focus] await remote.respond(request) // [!code focus] // Send an explicit result. // [!code focus] await remote.respond(request, { result: '0x...' }) // [!code focus] // Send an explicit error. // [!code focus] await remote.respond(request, { // [!code focus] error: { code: 4001, message: 'User rejected' }, // [!code focus] }) // [!code focus] ``` ## SSR In SSR environments (no `window`), prefer `Remote.noop()` which returns an inert context whose methods are no-ops. # `Remote.useEnsureVisibility` React hook that monitors element visibility using [IntersectionObserver v2](https://web.dev/articles/intersectionobserver-v2). When the wallet is rendered inside an iframe on an **untrusted host**, this hook detects occlusion / clickjacking attempts and exposes a `visible` flag plus an `invokePopup` action so the wallet can fall back to a popup. The hook is a no-op when the host is in [`remote.trustedHosts`](/docs/api/remote.create#trustedhosts), or when `enabled` is `false`. If IntersectionObserver v2 is not supported by the browser, `visible` is set to `false` so the wallet can immediately switch to a popup. ## Usage ```tsx import { Remote } from 'accounts/react' import { remote } from '../lib/config.js' function EnsureVisibility(props: { children: React.ReactNode }) { const mode = Remote.useState(remote, (s) => s.mode) const { invokePopup, ref, visible } = Remote.useEnsureVisibility(remote, { enabled: mode === 'iframe', }) return (
{visible ? props.children : }
) } ``` ## Parameters ### remote * **Type:** `Remote.Remote` * **Required** The remote context returned from [`Remote.create`](/docs/api/remote.create). Used to read `trustedHosts` and to send the `switch-mode` message when `invokePopup` is called. ```tsx import { Remote } from 'accounts/react' import { remote } from '../lib/config.js' const { invokePopup, ref, visible } = Remote.useEnsureVisibility( remote, // [!code focus] ) ``` ### options.enabled * **Type:** `boolean` * **Default:** `true` Whether visibility monitoring is enabled. Wire this to `mode === 'iframe'` so the hook is inert when the wallet is already rendered in a popup. ```tsx import { Remote } from 'accounts/react' import { remote } from '../lib/config.js' const mode = Remote.useState(remote, (s) => s.mode) const { invokePopup, ref, visible } = Remote.useEnsureVisibility(remote, { enabled: mode === 'iframe', // [!code focus] }) ``` ## Returns ### invokePopup * **Type:** `() => void` Sends a `switch-mode` message to the host SDK requesting it tear down the iframe and reopen the wallet in a popup. ### ref * **Type:** `React.RefObject` Ref that must be attached to the element being monitored for occlusion. ### visible * **Type:** `boolean` Whether the monitored element is currently fully visible. `true` while it has a clear viewport intersection of at least 99%; `false` when it is covered or when IntersectionObserver v2 is unavailable. # `Remote.useState` React hook that subscribes to the [`Remote`](/docs/api/remote.create) context's Zustand store. Re-renders whenever the selected slice changes. ## Usage ```ts import { Remote } from 'accounts/react' import { remote } from '../lib/config.js' // Subscribe to the full state. const state = Remote.useState(remote) // Or subscribe to a slice via a selector. const ready = Remote.useState(remote, (s) => s.ready) ``` ## Parameters ### remote * **Type:** `Remote.Remote` * **Required** The remote context returned from [`Remote.create`](/docs/api/remote.create). ```ts import { Remote } from 'accounts/react' import { remote } from '../lib/config.js' const state = Remote.useState(remote) // [!code focus] ``` ### selector * **Type:** `(state: Remote.State) => selected` * **Optional** Function that picks a slice from the store's state. The component only re-renders when the selected value changes (using `Object.is` equality). ```ts import { Remote } from 'accounts/react' import { remote } from '../lib/config.js' const ready = Remote.useState(remote, (s) => s.ready) // [!code focus] const mode = Remote.useState(remote, (s) => s.mode) // [!code focus] ``` ## Returns The full `Remote.State` when no `selector` is provided, otherwise the selector's return value: ```ts type State = { /** Whether the dialog is rendered in an iframe or popup. */ mode: 'iframe' | 'popup' | undefined /** Trusted host origin from MessageEvent. */ origin: string | undefined /** Whether the dialog is ready to display content. */ ready: boolean /** Queued RPC requests received from the host. */ requests: Remote.State['requests'] } ``` # `Remote.useTheme` React hook that applies theme overrides to the document root. On mount, it reads `accent`, `radius`, and `scheme` from the wallet app's URL search params and writes them to ``. When a `remote` context is provided, it also subscribes to live `theme` messages from the host SDK and re-applies the latest values. The hook captures the original document attributes/styles on mount and restores them on unmount, so it is safe to use inside a route that may unmount and remount. ## Usage ```tsx import { Remote } from 'accounts/react' import { remote } from '../lib/config.js' function App() { Remote.useTheme(remote) return } ``` ## Parameters ### remote * **Type:** `Remote.Remote | undefined` * **Optional** The remote context returned from [`Remote.create`](/docs/api/remote.create). When provided, the hook subscribes to live `theme` messages on the messenger so the wallet re-themes whenever the host SDK pushes an update. When omitted, only the initial URL-search-param theme is applied -- useful for SSR-rendered pages or static previews. ```tsx import { Remote } from 'accounts/react' import { remote } from '../lib/config.js' Remote.useTheme(remote) // [!code focus] ``` ## Theme values The following values are read from URL search params (and from `theme` messenger payloads) and applied to ``: ### accent * **Type:** `'neutral' | 'blue' | 'red' | 'amber' | 'green' | 'purple' | string` Sets the `data-theme-accent` attribute. When the value is not one of the named presets above, it is also written to the `--theme-accent` CSS custom property and `data-theme-accent` is set to `'custom'`. ### radius * **Type:** `string` Sets the `data-theme-radius` attribute on `` so theme CSS can pick up corner-radius variants. ### scheme * **Type:** `string` Written to `document.documentElement.style.colorScheme` (e.g. `'light'`, `'dark'`). # `Remote.validateSearch` Validates an RPC request from URL search params against the wallet's [`Schema.Request`](https://github.com/tempoxyz/accounts/blob/main/src/core/Schema.ts) discriminated union. * parses `search` against the schema * asserts the decoded `method` matches the expected `method` * enforces strict parameter schemas (e.g. required `limits`) On failure it rejects all pending requests via `remote.rejectAll(error)` and re-throws so the router can render its error boundary. Designed to be used from inside a TanStack Router (or similar) `validateSearch` callback so each per-method route gets a fully-typed, decoded request. ## Usage ```ts import { createFileRoute } from '@tanstack/react-router' import { Remote } from 'accounts' import { remote } from '../lib/config.js' export const Route = createFileRoute('/rpc/personal_sign')({ validateSearch: (search) => Remote.validateSearch(remote, search, { method: 'personal_sign' }), component: Component, }) ``` ## Parameters ### remote * **Type:** `Remote.Remote` * **Required** The remote context returned from [`Remote.create`](/docs/api/remote.create). Used to call `rejectAll(error)` when validation fails so the host SDK is notified before the router error boundary renders. ```ts import { Remote } from 'accounts' import { remote } from '../lib/config.js' Remote.validateSearch( remote, // [!code focus] search, { method: 'personal_sign' }, ) ``` ### search * **Type:** `Record` * **Required** Raw URL search params (including the encoded `_decoded`/`params`/`id` fields written by the host SDK). ```ts import { Remote } from 'accounts' Remote.validateSearch( remote, search, // [!code focus] { method: 'personal_sign' }, ) ``` ### parameters.method * **Type:** `Schema.Request['method']` * **Required** Expected RPC method name. The decoded request's `method` must equal this value or an `InvalidParamsError` is thrown. ```ts import { Remote } from 'accounts' Remote.validateSearch(remote, search, { method: 'eth_sendTransaction', // [!code focus] }) ``` ## Returns The validated, decoded request payload -- a discriminated union member matching the requested `method`, augmented with: * `id: number` -- coerced from `search.id`. * `jsonrpc: '2.0'`. * `_decoded` -- the parsed `Schema.Request` member. * `_returnType` -- phantom marker for the route's return type. ```ts type ReturnType = Extract< Schema.Request, { method: method } > & { id: number jsonrpc: '2.0' _decoded: Extract _returnType: unknown } ``` # Handlers Server handlers are framework-agnostic handlers that run on your backend to manage protocol operations that require server-side logic. Handlers are compatible with any server framework that supports the: * [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API), and is exposed via the `Handler#fetch` function * [Node.js `RequestListener` API](https://nodejs.org/api/http.html#http_class_http_serverrequestlistener), and is exposed via the `Handler#listener` function ```ts import { Handler } from 'accounts/server' import { privateKeyToAccount } from 'viem/accounts' const handler = Handler.relay({ feePayer: { account: privateKeyToAccount('0x...') }, }) createServer(handler.listener) // Node.js Bun.serve({ fetch: handler.fetch }) // Bun Deno.serve({ fetch: handler.fetch }) // Deno app.all('*', c => handler.fetch(c.request)) // Elysia app.use(handler.listener) // Express app.use(c => handler.fetch(c.req.raw)) // Hono export const GET = handler.fetch // Next.js export const POST = handler.fetch // Next.js ``` # `Handler.auth` Creates a server handler that issues SIWE-style authentication challenges and verifies signed messages. Mounts three POST endpoints under `path`: * `POST {path}/challenge` — issue a single-use challenge message (`{ message }`) * `POST {path}` — verify the signed message and issue a session (cookie via `Set-Cookie`, or `{ token }` body when `cookie: false` or the request opts in via `returnToken: true`) * `POST {path}/logout` — revoke the session and clear the cookie. Only mounted when [`session`](#session) is `true` (the default); otherwise calls fall through as `404`. The challenge wire format is [EIP-4361 (SIWE)](https://eips.ethereum.org/EIPS/eip-4361) for ecosystem compatibility, but address binding is deferred — the wallet supplies the signing address at verify time and the server uses it as the session subject. ## Usage ```ts import { Handler } from 'accounts/server' const handler = Handler.auth() ``` Then plug `handler` into your server framework of choice: ```ts createServer(handler.listener) // Node.js Bun.serve({ fetch: handler.fetch }) // Bun Deno.serve({ fetch: handler.fetch }) // Deno app.all('*', c => handler.fetch(c.request)) // Elysia app.use(handler.listener) // Express app.use(c => handler.fetch(c.req.raw)) // Hono export const GET = handler.fetch // Next.js export const POST = handler.fetch // Next.js ``` ### Validating & Extracting Sessions Use the [`getSession`](#getsession) helper attached to the handler to resolve the current session inside other routes on the same server. It reads the session cookie (or `Authorization: Bearer ` header) and returns the [`SessionPayload`](#sessionpayload), or `undefined` when the request is unauthenticated. ```ts import { Handler } from 'accounts/server' const handler = Handler.auth() app.get('/me', async (req) => { const session = await handler.getSession(req) if (!session) return new Response('unauthorized', { status: 401 }) return Response.json({ address: session.address }) }) ``` ## Return Type `Handler.auth` returns a [`Handler`](/docs/server/handler.compose#handler) extended with a `getSession` helper for resolving the current session from a follow-up request: ```ts type ReturnType = Handler & { getSession: (req: Request) => Promise } ``` #### `getSession` Resolves the current session from a request's session cookie or `Authorization: Bearer ` header. Returns `undefined` when no token is present, the token has been revoked, or the session has been disabled (`session: false`). ```ts import { Handler } from 'accounts/server' const handler = Handler.auth() // Inside another route on the same server: app.get('/me', async (req) => { const session = await handler.getSession(req) if (!session) return new Response('unauthorized', { status: 401 }) return Response.json({ address: session.address }) }) ``` ### `SessionPayload` ```ts type SessionPayload = { /** Address of the account that signed the challenge. */ address: Address /** Chain ID echoed into the challenge message. */ chainId: number /** * Verified email, set when the verify request included a valid identity * token and [`identity`](#identity) is configured. Omitted otherwise. */ email?: string | undefined /** Unix timestamp (seconds) when the session was issued. */ issuedAt: number /** Unix timestamp (seconds) when the session expires. */ expiresAt: number } ``` The session payload is persisted in [`store`](#store) under `session:{token}` and is also the value returned by [`getSession`](#getsession). ## Parameters ### cookie * **Type:** `boolean` * **Default:** `true` Whether to issue a session cookie on successful verify. When `false`: * the verify response always contains `{ token }` in the body (the per-request `returnToken` flag is ignored) * no `Set-Cookie` header is sent * logout does not clear a cookie * `getSession` ignores any incoming cookie -- only `Authorization: Bearer ` is honored Use this when the SDK lives in a non-browser context or the host app already manages its own auth cookies. ### cookieName * **Type:** `string` * **Default:** `'accounts_auth'` Cookie name for the session token. ```ts twoslash import { Handler } from 'accounts/server' const handler = Handler.auth({ cookieName: 'my_app_session', // [!code focus] }) ``` ### domain * **Type:** `string` * **Default:** request `Host` header Domain echoed into the SIWE challenge message. When omitted, the handler derives it from the request `Host` header (or, if [`origin`](#origin) is set, from that URL). ```ts twoslash import { Handler } from 'accounts/server' const handler = Handler.auth({ domain: 'app.example.com', // [!code focus] }) ``` ### identity * **Type:** `{ issuer?: string | undefined; required?: boolean | undefined }` * **Optional** Verified-email identity (OIDC), **enabled by default**. A verify request carrying an `idToken` (minted by the wallet during [`wallet_connect`](/docs/rpc/wallet_connect#requesting-a-verified-email)) is verified against the issuer's JWKS, with `aud` pinned to this request's resolved origin, `sub` cross-checked against the signed-in address, and `nonce` matched to the SIWE nonce. On success the verified email is folded onto the session as [`SessionPayload.email`](#sessionpayload). The wallet only mints tokens when the app requests identity, so requests without one are unaffected — they still issue a session, just without an email. You only need to set this option to customize the behavior below. Reusing the SIWE nonce as the token nonce means the single-use challenge store covers replay protection for both the signature and the token, so your app needs no separate nonce plumbing. * `issuer` — the wallet's identity issuer URL (whose JWKS signs the token). Defaults to `https://wallet.tempo.xyz/api/oidc`; override for a self-hosted wallet. * `required` — when `true`, reject the verify if no valid token is supplied (defaults to `false`, attach the email when present and proceed otherwise). ```ts twoslash import { Handler } from 'accounts/server' const handler = Handler.auth({ identity: { required: true }, // [!code focus] }) ``` For verifying a token outside of `Handler.auth` (for example when minting your own session in [`onAuthenticate`](#onauthenticate) with `session: false`), use [`Identity.verify`](/docs/server/identity.verify). ### onAuthenticate * **Type:** `(params: { address: Address; chainId: number; message: string; request: Request; signature: Hex }) => Response | Promise | void | Promise` * **Optional** Called after a successful SIWE signature verification, before the session token is issued. * Returning a `Response` merges its JSON body and status onto the default verify response. * Returning `void` (or a `Promise`) leaves the default response untouched. * Throwing rejects the request with `401`. The thrown error's `message` is surfaced as the response `error` field, the challenge is consumed, and no session is issued. ```ts twoslash import { Handler } from 'accounts/server' declare function isBlocked(address: string): Promise // ---cut--- const handler = Handler.auth({ onAuthenticate: async ({ address }) => { // [!code focus] if (await isBlocked(address)) throw new Error('address blocked') // [!code focus] }, // [!code focus] }) ``` ### origin * **Type:** `string` * **Optional** Pinned canonical public origin (e.g. `'https://app.example.com'`). When set, the SIWE `domain` and `uri`, and the cookie `Secure` flag, are derived from this URL — request `Host`, request URL, and `x-forwarded-*` headers are ignored. Recommended for production deployments behind a CDN or reverse proxy: it prevents a spoofed `x-forwarded-host` from shifting the SIWE domain and a spoofed `x-forwarded-proto: http` from disabling `Secure`. ```ts twoslash import { Handler } from 'accounts/server' const handler = Handler.auth({ origin: 'https://app.example.com', // [!code focus] }) ``` ### path * **Type:** `string` * **Default:** `'/'` Path prefix for the auth endpoints. ```ts twoslash import { Handler } from 'accounts/server' const handler = Handler.auth({ path: '/auth', // [!code focus] }) ``` ### session * **Type:** `boolean` * **Default:** `true` Whether to issue a session on successful verify. When `false`, verify acts as a stateless signature check: * no token is generated * no entry is written to the session store * no cookie is sent The verify response is `{}`. `getSession` always returns `undefined` and the `/logout` route is not mounted (so it returns `404`). Use this when the host application mints its own session token (e.g. a JWT inside `onAuthenticate`). ### store * **Type:** [`Kv`](/docs/server/kv) * **Default:** `Kv.memory()` Backing store for both single-use challenges (nonces) and issued sessions. Keys are namespaced internally (`challenge:…`, `session:…`). :::warning For one-time-consume nonce semantics across multiple worker instances, use a linearizable backend such as [`Kv.durableObject`](/docs/server/kv.durableObject). [`Kv.cloudflare`](/docs/server/kv.cloudflare) is eventually consistent and is rejected at construction time when used as the auth store on Cloudflare Workers. ::: ```ts twoslash // @noErrors import { Handler, Kv } from 'accounts/server' const handler = Handler.auth({ store: Kv.durableObject(env.NONCE_DO), // [!code focus] }) ``` ### transport * **Type:** `Transport` (from `viem`) * **Default:** `http()` Viem transport for the Tempo client used to verify signatures. The client is always built against the `tempo` chain — Tempo's `chain.verifyHash` natively understands `SignatureEnvelope` for WebAuthn / P256 / keychain signatures and falls back to ECDSA recovery for plain EOAs. ```ts twoslash import { http } from 'viem' import { Handler } from 'accounts/server' const handler = Handler.auth({ transport: http('https://rpc.tempo.xyz'), // [!code focus] }) ``` ### trustProxy * **Type:** `boolean` * **Default:** `true` on Cloudflare Workers (always edge-fronted), `false` elsewhere. Honor `x-forwarded-proto` / `x-forwarded-host` to derive the public origin. Required when running behind a trusted reverse proxy that terminates TLS. When `false`, forwarded headers are ignored to prevent spoofing on deployments that expose the origin server directly. Ignored when [`origin`](#origin) is set. ```ts twoslash import { Handler } from 'accounts/server' const handler = Handler.auth({ trustProxy: true, // [!code focus] }) ``` ### ttl * **Type:** `{ challenge?: number; session?: number }` * **Optional** TTLs in seconds for stored challenges and issued sessions. #### ttl.challenge * **Type:** `number` * **Default:** `600` (10 minutes) Maximum age of a challenge in seconds before it expires. ```ts twoslash import { Handler } from 'accounts/server' const handler = Handler.auth({ ttl: { challenge: 60 * 5 }, // 5 minutes // [!code focus] }) ``` #### ttl.session * **Type:** `number` * **Default:** `86400` (24 hours) Lifetime of an issued session in seconds. After expiry, `getSession` returns `undefined` and the store entry is evicted by the underlying backend. ```ts twoslash import { Handler } from 'accounts/server' const handler = Handler.auth({ ttl: { session: 60 * 60 * 24 * 7 }, // 7 days // [!code focus] }) ``` ### cors * **Type:** `boolean | Cors` * **Default:** `true` Inherited from [`from.Options`](/docs/server/handler.compose#optionscors). CORS configuration applied to every response. ### headers * **Type:** `Headers | Record` * **Optional** Inherited from [`from.Options`](/docs/server/handler.compose#optionsheaders). Additional headers merged onto every response. # `Handler.compose` Composes multiple handlers into a single handler mounted on a fresh Hono app at `path` (default `/`). Routing is dispatched by Hono's trie deterministically per path — there is no "try each handler in order until one returns non-404" fallback. The returned handler preserves each sub-handler's route schema so the composed app stays usable from Hono's typed RPC client (see [Typed RPC](#typed-rpc) below). ## Usage ```ts import { Handler, Kv } from 'accounts/server' import { privateKeyToAccount } from 'viem/accounts' const handler = Handler.compose([ Handler.relay({ feePayer: { account: privateKeyToAccount('0x...') }, }), Handler.webAuthn({ kv: Kv.memory(), origin: 'https://example.com', rpId: 'example.com', }), ]) ``` Then plug `handler` into your server framework of choice: ```ts createServer(handler.listener) // Node.js Bun.serve({ fetch: handler.fetch }) // Bun Deno.serve({ fetch: handler.fetch }) // Deno app.all('*', c => handler.fetch(c.request)) // Elysia app.use(handler.listener) // Express app.use(c => handler.fetch(c.req.raw)) // Hono export const GET = handler.fetch // Next.js export const POST = handler.fetch // Next.js ``` ## Parameters ### handlers * **Type:** `readonly Handler[]` * **Required** Array of handlers to compose. Each handler is mounted at `options.path` on a fresh Hono app; Hono's trie picks the matching route deterministically. ### options * **Type:** `{ path?: string; cors?: boolean | Cors; headers?: Headers | Record }` * **Default:** `{}` Configuration for the composed app. Inherits the [`from.Options`](#cors) shape used by every other server handler. #### options.path * **Type:** `string` * **Default:** `'/'` Base path prefix where every sub-handler is mounted on the composed Hono app. #### options.cors * **Type:** `boolean | Cors` * **Default:** `true` CORS configuration applied to every response. * `true` — allow all origins with default methods/headers. * `false` — disable CORS headers entirely. * Object — fine-grained `{ origin?, methods?, headers?, credentials?, exposeHeaders?, maxAge? }` override. #### options.headers * **Type:** `Headers | Record` * **Optional** Additional headers merged onto every response (after CORS headers). ## Return Type ### Handler ```ts import type { Hono } from 'hono' type Handler = Hono> = app & { listener: (req: any, res: any) => void } ``` A `Handler` is the underlying Hono app extended with a Node.js `RequestListener` (`listener`). The generic `app` parameter flows the sub-handler route schemas through to consumers — most notably to Hono's typed RPC client (`hc`). ### ComposedSchema ```ts import type { ExtractSchema, MergeSchemaPath, Schema } from 'hono/types' type ComposedSchema ``` Helper that merges the route schemas of every sub-handler into a single Hono schema, prefixing each route key with `path`. `Handler.compose` uses this internally so the composed app's type carries every sub-handler's routes — you typically interact with it indirectly via `typeof handler`. ## Typed RPC Because the composed `Handler` is a Hono app, you can pass `typeof handler` to `hc` (re-exported from `accounts/server`) for a fully-typed RPC client: ```ts import { Handler, hc } from 'accounts/server' const handler = Handler.compose( [Handler.exchange(), Handler.relay()], { path: '/api' }, ) const client = hc('https://wallet.example.com') await client.api.exchange.quote.$post({ json: { /* ... */ } }) ``` # `Handler.deviceCode` Creates a Wata device-code handler for cross-device wallet RPC requests. The handler implements the OAuth 2.0 Device Authorization Grant with PKCE and returns approval-page RPC responses to the polling client. The handler is method-agnostic. The consuming host must decide which methods its approval UI accepts, as it does for window and `postMessage` transports. ## Usage ```ts import { Handler } from 'accounts/server' import { Store } from 'wata/host' const handler = Handler.deviceCode({ baseUrl: 'https://wallet.example.com', html: { render({ record, request, userCode }) { if (request.headers.get('accept')?.includes('application/json')) return Response.json({ record, user_code: userCode }) const search = userCode ? `?user_code=${encodeURIComponent(userCode)}` : '' return new Response(null, { headers: { location: `https://wallet.example.com/approve${search}` }, status: 302, }) }, }, store: Store.memory(), validate({ request, result }) { if (request.method !== 'wallet_connect') return Response.json({ error: 'unsupported_method' }, { status: 400 }) // Verify the submitted result against the pending request here. return undefined }, }) ``` Mount `handler.fetch` in any server framework that accepts Fetch API handlers. The default endpoint prefix is `/auth/device`: * `POST /auth/device/register` registers the authorization request. * `POST /auth/device/token` polls for the approved response. * `GET /auth/device/verify` calls `html.render`. * `POST /auth/device/verify` approves or denies the request. Use persistent shared storage in production. `Store.memory()` is suitable only for tests and single-process development. ## Approval Page Contract The `html.render` hook receives the pending record and consumer metadata. Render the approval page directly, or expose that data to a separate approval UI. The usage example returns the record as JSON when the request accepts JSON. Submit the approved result to `POST {path}/verify`: ```ts await fetch('/auth/device/verify', { body: JSON.stringify({ action: 'approve', results: [{ id: request.id, result }], user_code: 'ABCDEFGH', }), headers: { 'content-type': 'application/json' }, method: 'POST', }) ``` The handler matches the submitted result to the pending request by `id` before releasing the response. To relay an RPC failure, submit `{ id, error: { code, message, data? } }` instead of a result. To deny the request, omit `results` and send `action: 'deny'`. ## Parameters ### `baseUrl` * **Type:** `string | ((request: Request) => string)` * **Optional** Public origin used to build the verification URL. Use a function when a proxy or tenant determines the origin from each request. ### `expiresIn` * **Type:** `number` * **Default:** `600` Authorization intent lifetime in seconds. ### `fetch` * **Type:** `typeof globalThis.fetch` * **Optional** Fetch implementation used to resolve consumer discovery metadata. ### `html.render` * **Type:** `(options) => Response | Promise` * **Required** Renders or redirects the approval page for `GET {path}/verify`. The options include the pending record, consumer metadata, request, and user code. ### `maxBodyBytes` * **Type:** `number` * **Default:** `65536` Maximum JSON body size accepted by `POST {path}/verify`. ### `path` * **Type:** `string` * **Default:** `'/auth/device'` Endpoint prefix for device-code registration, polling, and verification. ### `pollingInterval` * **Type:** `number` * **Optional** Suggested polling interval in milliseconds returned to the client. ### `store` * **Type:** `Store` * **Default:** `Store.memory()` Wata store used for pending device codes and response envelopes. Use a shared persistent adapter for multi-instance deployments. ### `validate` * **Type:** `(options) => Response | undefined | Promise` * **Required** Applies host policy before the handler approves a submitted result. Protect the approval route with the host's authentication, allow only supported methods, and verify the result against the pending request. Return a response to reject the submission, or `undefined` to approve it. # `Handler.exchange` Creates a stablecoin-exchange handler that returns swap quotes plus the matching `calls` (approve + buy/sell) for the [Tempo Stablecoin DEX](https://docs.tempo.xyz/guide/stablecoin-dex). Mounts two endpoints under `path`: * `GET {path}/tokens` — list known tokens for a chain (defaults to the first configured chain; pass `?chainId=N` to pick a different one) * `POST {path}/quote` — return a quote (`token` + `pairToken` amounts, slippage) and ready-to-submit `calls` The route schema is preserved on the return type so consumers can derive a typed RPC client with [`hc`](/docs/server/hc). ## Usage ```ts twoslash import { Handler } from 'accounts/server' const handler = Handler.exchange() ``` Then plug `handler` into your server framework of choice: ```ts twoslash // @noErrors import { Handler } from 'accounts/server' const handler = Handler.exchange() // ---cut--- createServer(handler.listener) // Node.js Bun.serve({ fetch: handler.fetch }) // Bun Deno.serve({ fetch: handler.fetch }) // Deno app.all('*', c => handler.fetch(c.request)) // Elysia app.use(handler.listener) // Express app.use(c => handler.fetch(c.req.raw)) // Hono export const GET = handler.fetch // Next.js export const POST = handler.fetch // Next.js ``` ## Typed RPC Pass `typeof handler` to [`hc`](/docs/server/hc) for a fully-typed RPC client: ```ts twoslash import { Handler, hc } from 'accounts/server' const handler = Handler.exchange() const client = hc('https://example.com') const res = await client.exchange.quote.$post({ json: { amount: '1', pairToken: 'pathUSD', slippage: 0.01, token: 'USDC', type: 'sell', }, }) if (res.ok) { const { calls, pairToken, token } = await res.json() } ``` ## Parameters ### cacheTtl * **Type:** `number` * **Default:** `600` (10 minutes) TTL in seconds for cached tokenlist responses. On-chain token metadata is cached without expiry (immutable per address). ```ts twoslash import { Handler } from 'accounts/server' const handler = Handler.exchange({ cacheTtl: 60, // [!code focus] }) ``` ### chains * **Type:** `readonly [Chain, ...Chain[]]` (from `viem`) * **Default:** `[tempo, tempoModerato, tempoDevnet]` Supported chains. The first chain is used to resolve the default client when a request omits `chainId`. ```ts twoslash import { tempo } from 'viem/tempo/chains' import { Handler } from 'accounts/server' const handler = Handler.exchange({ chains: [tempo], // [!code focus] }) ``` ### kv * **Type:** [`Kv`](/docs/server/kv) * **Default:** `Kv.memory()` Key-value store used to cache tokenlist and on-chain metadata responses across requests. Provide [`Kv.cloudflare(env.KV)`](/docs/server/kv.cloudflare) for cross-instance caching, or omit for an in-process cache. Shared with [`Handler.relay`](/docs/server/handler.relay) — providing the same adapter on both handlers reuses a single cache. ```ts twoslash // @noErrors import { Handler, Kv } from 'accounts/server' const handler = Handler.exchange({ kv: Kv.cloudflare(env.KV), // [!code focus] }) ``` ### onRequest * **Type:** `(request: Request) => void | Promise` * **Optional** Callback called before processing each request. Useful for logging, custom rate limiting, or auth. ```ts twoslash import { Handler } from 'accounts/server' const handler = Handler.exchange({ onRequest: async (request) => { // [!code focus] console.log('Exchange request:', request.url) // [!code focus] }, // [!code focus] }) ``` ### path * **Type:** `string` * **Default:** `'/exchange'` Path prefix for the exchange endpoints. ```ts twoslash import { Handler } from 'accounts/server' const handler = Handler.exchange({ path: '/swap', // [!code focus] }) ``` ### resolveTokens * **Type:** `(chainId: number) => readonly Token[] | Promise` * **Default:** Fetches `https://tokenlist.tempo.xyz/list/:chainId` Resolves the list of known tokens for a chain. Used to resolve symbol references (e.g. `"USDC.e"`) to addresses + metadata. Address references are matched against this list first, falling back to on-chain metadata. ```ts twoslash import { Handler } from 'accounts/server' const handler = Handler.exchange({ resolveTokens: (chainId) => [ // [!code focus] { // [!code focus] address: '0x20c0000000000000000000000000000000000001', // [!code focus] decimals: 6, // [!code focus] name: 'pathUSD', // [!code focus] symbol: 'pathUSD', // [!code focus] }, // [!code focus] ], // [!code focus] }) ``` ### transports * **Type:** `Record` (from `viem`) * **Default:** `http()` for each chain Transports keyed by chain ID. ```ts twoslash import { http } from 'viem' import { tempo } from 'viem/tempo/chains' import { Handler } from 'accounts/server' const handler = Handler.exchange({ transports: { // [!code focus] [tempo.id]: http('https://rpc.tempo.xyz'), // [!code focus] }, // [!code focus] }) ``` ### cors * **Type:** `boolean | Cors` * **Default:** `true` Inherited from [`from.Options`](/docs/server/handler.compose#optionscors). CORS configuration applied to every response. ### headers * **Type:** `Headers | Record` * **Optional** Inherited from [`from.Options`](/docs/server/handler.compose#optionsheaders). Additional headers merged onto every response. # `Handler.relay` Creates a server handler that proxies certain RPC requests (like `eth_fillTransaction`) with wallet-aware enrichment — fee token resolution, simulation-based balance diffs, conditional sponsorship, and automatic AMM resolution for insufficient balances. ## Usage ```ts twoslash import { Handler } from 'accounts/server' const handler = Handler.relay() ``` Then plug `handler` into your server framework of choice: ```ts twoslash // @noErrors import { Handler } from 'accounts/server' const handler = Handler.relay() // ---cut--- createServer(handler.listener) // Node.js Bun.serve({ fetch: handler.fetch }) // Bun Deno.serve({ fetch: handler.fetch }) // Deno app.all('*', c => handler.fetch(c.request)) // Elysia app.use(handler.listener) // Express app.use(c => handler.fetch(c.req.raw)) // Hono export const GET = handler.fetch // Next.js export const POST = handler.fetch // Next.js ``` ## Features ### Sponsorship Configure a [`feePayer`](#feepayer) to sponsor transactions. The relay signs `feePayerSignature` on the filled transaction and returns sponsor details in the response metadata. Use a [`validate`](#feepayervalidate) callback for conditional sponsorship — rejected transactions are re-filled for self-payment. :::code-group ```ts twoslash [server.ts] import { privateKeyToAccount } from 'viem/accounts' import { Handler } from 'accounts/server' const blockedAddress = '0x...' // ---cut--- const handler = Handler.relay({ feePayer: { account: privateKeyToAccount('0x...'), name: 'My App', url: 'https://myapp.com', // Optional — validate sponsorship approval. validate: (request) => request.from !== blockedAddress, }, }) ``` ```ts twoslash [client.ts] // @noErrors import { Provider } from 'accounts' const provider = Provider.create() const [account] = await provider.request({ method: 'eth_accounts' }) // ---cut--- const result = await provider.request({ method: 'eth_fillTransaction', params: [{ from: account, calls: [{ to: '0x20c000000000000000000000b9537d11c60e8b50', // USDC.e // transfer 100 USDC.e data: '0xa9059cbb000000000000000000000000cafebabecafebabecafebabecafebabecafebabe0000000000000000000000000000000000000000000000000000000005f5e100', }], }], }) result.capabilities.sponsored // true result.capabilities.sponsor // { // address: '0x1234567890abcdef1234567890abcdef12345678', // name: 'My App', // url: 'https://myapp.com', // } ``` ::: ### Auto Swap When a user has insufficient balance of a required token, the relay automatically injects swap calls (approve + buy) via the [Stablecoin DEX](https://docs.tempo.xyz/guide/stablecoin-dex). Swap details are reported in `capabilities.autoSwap` (including the prepended `calls`), and swap-related balance diffs are excluded from `capabilities.balanceDiffs`. Auto-swap is enabled when either [`autoSwap`](#autoswap) is set to a truthy value OR [`features: 'all'`](#features) is set. :::code-group ```ts twoslash [client.ts] // @noErrors import { Provider } from 'accounts' const provider = Provider.create() const [account] = await provider.request({ method: 'eth_accounts' }) // ---cut--- const result = await provider.request({ method: 'eth_fillTransaction', params: [{ from: account, calls: [{ to: '0x20c000000000000000000000b9537d11c60e8b50', // USDC.e // transfer 100 USDC.e data: '0xa9059cbb000000000000000000000000cafebabecafebabecafebabecafebabecafebabe0000000000000000000000000000000000000000000000000000000005f5e100', }], }], }) result.capabilities.autoSwap // { // calls: [ // // Stablecoin DEX `approve(...)` call. // { to: '0x20c0...', data: '0x095ea7b3...' }, // // Stablecoin DEX `buy(...)` call. // { to: '0x20c0...', data: '0x...' }, // ], // maxIn: { // decimals: 6, // formatted: '105.000000', // name: 'pathUSD', // symbol: 'pathUSD', // token: '0x20c0000000000000000000000000000000000001', // value: '0x6422c40', // }, // minOut: { // decimals: 6, // formatted: '100.000000', // name: 'USDC.e', // symbol: 'USDC.e', // token: '0x20c000000000000000000000b9537d11c60e8b50', // value: '0x5f5e100', // }, // slippage: 0.05, // } ``` ```ts [server.ts] import { Handler } from 'accounts/server' const handler = Handler.relay({ autoSwap: { slippage: 0.05 }, // 5% (default) }) ``` ::: ### Best Fee Tokens Picks the user's optimal `feeToken` automatically. Internally, `resolveFeeToken` walks the candidates in this order and returns the first match: 1. **Explicit `feeToken`** — if the request (or sponsor's `feePayer.feeToken`) supplies a `feeToken`, it is used as-is and the rest of the resolution is skipped. 2. **On-chain preference (with balance)** — `fee.getUserToken(account)` (cached via [`kv`](#kv) for ~60s under `fee.userToken:{chainId}:{account}`). If the user has set a preference, `resolveFeeToken` uses it when the user holds a balance — first by checking the [`resolveTokens`](#resolvetokens) list, then falling back to a direct on-chain `balanceOf` for the preferred token if it isn't in the list. 3. **Highest-balance token** — across the [`resolveTokens`](#resolvetokens) list (extended with any TIP20 token addressed by the transaction's calls so a user transferring `USDC.e` can pay gas in `USDC.e` even when the configured list defaults to `pathUSD`). 4. **Undefined** — when no candidate has a positive balance, `resolveFeeToken` returns `undefined` and the relay falls back to the chain's first configured fee token (`getTokens(chainId)[0]`). ```ts twoslash // @noErrors import { Provider } from 'accounts' const provider = Provider.create() const [account] = await provider.request({ method: 'eth_accounts' }) // ---cut--- const result = await provider.request({ method: 'eth_fillTransaction', params: [{ from: account, calls: [{ to: '0x20c000000000000000000000b9537d11c60e8b50', // USDC.e // transfer 100 USDC.e data: '0xa9059cbb000000000000000000000000cafebabecafebabecafebabecafebabecafebabe0000000000000000000000000000000000000000000000000000000005f5e100', }], }], }) result.capabilities.fee // { // amount: '0x6b86', // decimals: 6, // formatted: '0.027526', // symbol: 'USDC.e', // } ``` ### Balance Diffs Simulates the transaction and returns per-account token balance diffs in `capabilities.balanceDiffs`. :::code-group ```ts twoslash [client.ts] // @noErrors import { Provider } from 'accounts' const provider = Provider.create() const [account] = await provider.request({ method: 'eth_accounts' }) // ---cut--- const result = await provider.request({ method: 'eth_fillTransaction', params: [{ from: account, calls: [{ to: '0x20c000000000000000000000b9537d11c60e8b50', // USDC.e // transfer 100 USDC.e data: '0xa9059cbb000000000000000000000000cafebabecafebabecafebabecafebabecafebabe0000000000000000000000000000000000000000000000000000000005f5e100', }], }], }) result.capabilities.balanceDiffs // { // '0x1234567890abcdef1234567890abcdef12345678': [{ // address: '0x20c000000000000000000000b9537d11c60e8b50', // decimals: 6, // direction: 'outgoing', // formatted: '100.000000', // name: 'USDC.e', // recipients: ['0xcafebabecafebabecafebabecafebabecafebabe'], // symbol: 'USDC.e', // value: '0x5f5e100', // }], // } ``` ```ts twoslash [server.ts] import { Handler } from 'accounts/server' // ---cut--- // Balance diffs are included by default — no extra config needed. const handler = Handler.relay() ``` ::: ### Fee Derivation Derives fee estimates from the filled transaction and returns them as both raw and human-readable values, so the UI can display costs without additional computation. :::code-group ```ts twoslash [client.ts] // @noErrors import { Provider } from 'accounts' const provider = Provider.create() const [account] = await provider.request({ method: 'eth_accounts' }) // ---cut--- const result = await provider.request({ method: 'eth_fillTransaction', params: [{ from: account, calls: [{ to: '0x20c000000000000000000000b9537d11c60e8b50', // USDC.e // transfer 100 USDC.e data: '0xa9059cbb000000000000000000000000cafebabecafebabecafebabecafebabecafebabe0000000000000000000000000000000000000000000000000000000005f5e100', }], }], }) result.capabilities.fee // { // amount: '0x6b86', // decimals: 6, // formatted: '0.027526', // symbol: 'USDC.e', // } ``` ```ts twoslash [server.ts] import { Handler } from 'accounts/server' // ---cut--- // Formatted fees are included by default — no extra config needed. const handler = Handler.relay() ``` ::: ### Require Funds For insufficient balance errors, the relay also returns a `capabilities.requireFunds` object with the exact deficit amount and token metadata — enabling UIs to prompt the user to fund their account. ```ts twoslash // @noErrors import { Provider } from 'accounts' const provider = Provider.create() const [account] = await provider.request({ method: 'eth_accounts' }) // ---cut--- const result = await provider.request({ method: 'eth_fillTransaction', params: [{ from: account, calls: [{ to: '0x20c000000000000000000000b9537d11c60e8b50', // USDC.e // transfer 100 USDC.e (but account has 40) data: '0xa9059cbb000000000000000000000000cafebabecafebabecafebabecafebabecafebabe0000000000000000000000000000000000000000000000000000000005f5e100', }], }], }) result.capabilities.requireFunds // { // amount: '0x3938700', // decimals: 6, // formatted: '60.000000', // token: '0x20c000000000000000000000b9537d11c60e8b50', // symbol: 'USDC.e', // } result.capabilities.balanceDiffs // { // '0x1234567890abcdef1234567890abcdef12345678': [{ // address: '0x20c000000000000000000000b9537d11c60e8b50', // decimals: 6, // direction: 'outgoing', // formatted: '100.000000', // name: 'USDC.e', // recipients: ['0xcafebabecafebabecafebabecafebabecafebabe'], // symbol: 'USDC.e', // value: '0x5f5e100', // }], // } ``` ### Enabling Features By default, only a minimum set of features are enabled, given on what options you pass to `Handler.relay(){:js}` (e.g. `feePayer`, `autoSwap`, etc). Set [`features: 'all'`](#features) to enable all features by default such as: **fee token resolution**, **auto-swap**, and **simulation** (balance diffs + fee breakdown). This will come at the cost of slightly increased network latency. ```ts twoslash // @noErrors import { privateKeyToAccount } from 'viem/accounts' import { Handler } from 'accounts/server' const handler = Handler.relay({ features: 'all', // [!code focus] }) ``` ## Parameters ### autoSwap * **Type:** `false | { slippage?: number }` AMM swap options for automatic insufficient balance resolution. When a user doesn't hold enough of a token, the relay auto-swaps from their fee token via the [Stablecoin DEX](https://docs.tempo.xyz/guide/stablecoin-dex). Set to `false` to disable even when `features: 'all'` is set. ```ts twoslash import { Handler } from 'accounts/server' const handler = Handler.relay({ autoSwap: { slippage: 0.02 }, // 2% slippage // [!code focus] }) ``` #### autoSwap.slippage * **Type:** `number` * **Default:** `0.05` (5%) Slippage tolerance for AMM swaps. The relay sets `maxAmountIn = deficit + deficit * slippage`. ### cacheTtl * **Type:** `number` * **Default:** `600` (10 minutes) TTL in seconds for cached [`resolveTokens`](#resolvetokens) results. Cached entries are stored in [`kv`](#kv). ```ts twoslash import { Handler } from 'accounts/server' const handler = Handler.relay({ cacheTtl: 60, // [!code focus] }) ``` ### chains * **Type:** `readonly [Chain, ...Chain[]]` * **Default:** `[tempo, tempoModerato, tempoDevnet]` Supported chains. The handler resolves the client based on the `chainId` in the incoming transaction. ```ts twoslash import { tempo } from 'viem/tempo/chains' import { Handler } from 'accounts/server' const handler = Handler.relay({ chains: [tempo], // [!code focus] }) ``` ### features * **Type:** `'all'` * **Optional** Controls which relay features are enabled. By default, only fee payer sponsorship is active. * `'all'`: enables all features (fee token resolution, auto-swap, balance diffs, fee breakdown, etc). * `undefined` (default): enables only features that are configured via options (e.g. `feePayer`, `autoSwap`, etc). ```ts twoslash // @noErrors import { Handler } from 'accounts/server' const handler = Handler.relay({ features: 'all', // [!code focus] }) ``` ### feePayer * **Type:** `object` * **Optional** Fee payer configuration. When provided, the relay will sign `feePayerSignature` on the filled transaction. ```ts twoslash import { privateKeyToAccount } from 'viem/accounts' import { Handler } from 'accounts/server' const handler = Handler.relay({ feePayer: { // [!code focus] account: privateKeyToAccount('0x...'), // [!code focus] name: 'My App', // [!code focus] url: 'https://myapp.com', // [!code focus] }, // [!code focus] }) ``` #### feePayer.account * **Type:** `LocalAccount` * **Required** The account to use as the fee payer. #### feePayer.feeToken * **Type:** `Address` * **Optional** Sponsor's preferred fee-token override. When sponsoring, this overrides whatever `feeToken` the consumer asked for — the sponsor is paying, so the sponsor picks. Falls back to the request's `feeToken` when omitted. #### feePayer.name * **Type:** `string` * **Optional** Sponsor display name returned in the response metadata. #### feePayer.url * **Type:** `string` * **Optional** Sponsor URL returned in the response metadata. #### feePayer.validate * **Type:** `(request: Transaction.TransactionRequest) => boolean | Promise` * **Optional** Validates whether to sponsor a transaction. The `request` parameter is a [`Transaction.TransactionRequest`](https://wevm.dev/viem/tempo/Transaction.TransactionRequest) from `viem/tempo` — the typed Tempo envelope built for sponsorship signing. When omitted, all transactions are sponsored. Return `false` to reject sponsorship — the relay will re-fill without `feePayer` so gas/nonce are correct for self-payment. ```ts twoslash import { privateKeyToAccount } from 'viem/accounts' import { Handler } from 'accounts/server' const blocked = '0x...' const handler = Handler.relay({ feePayer: { account: privateKeyToAccount('0x...'), validate: (request) => request.from !== blocked, // [!code focus] }, }) ``` ### kv * **Type:** [`Kv`](/docs/server/kv) * **Default:** `Kv.memory()` Key-value store used to cache `resolveTokens` results across requests, and to cache the `fee.getUserToken` lookup used by [Best Fee Tokens](#best-fee-tokens). Shared with `Handler.exchange` — providing `Kv.cloudflare(env.KV)` (or any other adapter) on both handlers reuses a single cache. ```ts twoslash // @noErrors import { Handler, Kv } from 'accounts/server' const handler = Handler.relay({ kv: Kv.cloudflare(env.KV), // [!code focus] }) ``` ### onRequest * **Type:** `(request: RpcRequest) => Promise` * **Optional** Callback called before processing each request. Useful for logging, rate limiting, or custom validation. ```ts twoslash import { Handler } from 'accounts/server' const handler = Handler.relay({ onRequest: async (request) => { // [!code focus] console.log('Processing request:', request.method) // [!code focus] }, // [!code focus] }) ``` ### path * **Type:** `string` * **Default:** `'/'` Path where the handler listens for requests. ```ts twoslash import { Handler } from 'accounts/server' const handler = Handler.relay({ path: '/relay', // [!code focus] }) ``` When used with [`Provider.relay`](/docs/api/provider#relay), expose both the base path and chain-qualified child paths from your HTTP router. The provider posts JSON-RPC to `/relay/:chainId`; direct `POST /relay` calls are only valid when your router sends that exact path to the relay handler. ### resolveTokens * **Type:** `(chainId: number) => readonly Token[] | Promise` * **Default:** Fetches `https://tokenlist.tempo.xyz/list/:chainId` Resolves the list of tokens to check balances for during fee token resolution. The relay checks `balanceOf` for each token and picks the one with the highest balance. ```ts twoslash import { Handler } from 'accounts/server' const handler = Handler.relay({ resolveTokens: (chainId) => [ // [!code focus] { // [!code focus] address: '0x20c0000000000000000000000000000000000000', // [!code focus] decimals: 6, // [!code focus] name: 'pathUSD', // [!code focus] symbol: 'pathUSD', // [!code focus] }, // [!code focus] { // [!code focus] address: '0x20c000000000000000000000b9537d11c60e8b50', // [!code focus] decimals: 6, // [!code focus] name: 'USDC.e', // [!code focus] symbol: 'USDC.e', // [!code focus] }, // [!code focus] ], // [!code focus] }) ``` ### transports * **Type:** `Record` * **Default:** `http()` for each chain Transports keyed by chain ID. ```ts twoslash import { http } from 'viem' import { tempo } from 'viem/tempo/chains' import { Handler } from 'accounts/server' const handler = Handler.relay({ transports: { // [!code focus] [tempo.id]: http('https://rpc.tempo.xyz'), // [!code focus] }, // [!code focus] }) ``` ### cors * **Type:** `boolean | Cors` * **Default:** `true` Inherited from [`from.Options`](/docs/server/handler.compose#optionscors). CORS configuration applied to every response. ### headers * **Type:** `Headers | Record` * **Optional** Inherited from [`from.Options`](/docs/server/handler.compose#optionsheaders). Additional headers merged onto every response. ## Request The relay accepts standard JSON-RPC `eth_fillTransaction` requests. In addition to the chain's parameter shape, the relay reads the following wallet-level fields from the first parameter object: * `from` (`Address`) — used for fee-token resolution and sponsor validation. * `feeToken` (`Address`) — explicit fee token request. Wins over `resolveTokens`-based resolution. The sponsor's [`feePayer.feeToken`](#feepayerfeetoken) overrides this when sponsoring. * `feePayer` (`string | false`): * **`string`** — an external fee-payer URL. The relay forwards the fill through that URL (so a third-party sponsor service can sign), and re-emits its `capabilities.sponsor` and (if present) `capabilities.autoSwap` on the response. The handler's own [`feePayer`](#feepayer) is *not* required — providing an external URL alone is enough to enable sponsorship for the request. * **`false`** — opt out of sponsorship for this request even if the relay has [`feePayer`](#feepayer) configured. * **omitted** — sponsorship is applied when [`feePayer`](#feepayer) is configured on the handler. * `capabilities.balanceDiffs` (`boolean`, default `true`) — set to `false` to skip the post-fill `tempo_simulateV1` round trip that derives [Balance Diffs](#balance-diffs) (~250-400ms saving). * `capabilities.errors` (`boolean`) — when `true`, a failed fill returns a structured `capabilities.error` instead of a raw RPC error. ## Response The relay enriches the standard `eth_fillTransaction` response with a `capabilities` object: ```ts type Response = { /** Wallet-specific capabilities computed during fill. */ capabilities: { /** Per-account balance diffs from simulation (swap-related diffs excluded). */ balanceDiffs?: { [account: Address]: BalanceDiff[] } /** Fee estimate for the transaction. */ fee: { /** Raw fee amount in token units (hex-encoded). */ amount: Hex /** Token decimals (e.g. 6). */ decimals: number /** Human-readable fee (e.g. "0.028022"). */ formatted: string /** Token symbol (e.g. "USDC.e"). */ symbol: string } | null /** AMM swap injected to cover an insufficient balance. */ autoSwap?: { /** Stablecoin DEX calls prepended to the transaction (approve + buy). */ calls: readonly { to: Address; data: Hex }[] /** Max input amount with slippage. */ maxIn: SwapAmount /** Deficit amount that triggered the swap. */ minOut: SwapAmount /** Slippage tolerance (e.g. 0.05 = 5%). */ slippage: number } /** Structured error details when the fill fails (e.g. InsufficientBalance). */ error?: { /** ABI item that caused the error. */ abiItem: AbiItem /** Data that caused the error. */ data: Hex /** Revert error name (e.g. "InsufficientBalance"). */ errorName: string /** Human-readable error message. */ message: string } /** Funding requirement when InsufficientBalance is encountered. */ requireFunds?: { /** Deficit amount in token units (hex-encoded). */ amount: Hex /** Token decimals (e.g. 6). */ decimals: number /** Human-readable deficit (e.g. "100.000000"). */ formatted: string /** Token address. */ token: Address /** Token symbol (e.g. "USDC.e"). */ symbol: string } /** Sponsor details (when sponsored). */ sponsor?: { address: Address; name: string; url: string } /** Whether the transaction is sponsored by a fee payer. */ sponsored: boolean } /** Fully filled transaction. */ tx: { // ...filled tx fields /** Resolved fee token used for this transaction. */ feeToken: Address } } type BalanceDiff = { /** Token address. */ address: Address /** Token decimals (e.g. 6). */ decimals: number /** Direction relative to the user. */ direction: 'incoming' | 'outgoing' /** Human-readable formatted amount (e.g. "100.00"). */ formatted: string /** Token name (e.g. "USDC.e"). */ name: string /** Addresses receiving this asset. */ recipients: readonly Address[] /** Token symbol (e.g. "USDC.e"). */ symbol: string /** Token amount (hex-encoded). */ value: Hex } type SwapAmount = { /** Token decimals. */ decimals: number /** Human-readable formatted amount. */ formatted: string /** Token name (e.g. "pathUSD"). */ name: string /** Token symbol (e.g. "pathUSD"). */ symbol: string /** Token address. */ token: Address /** Amount (hex-encoded). */ value: Hex } ``` # `Handler.webAuthn` Creates a WebAuthn ceremony handler that manages registration and authentication flows server-side. :::info [See the guide](https://docs.tempo.xyz/guide/use-accounts/embed-passkeys) ::: Mounts 5 POST endpoints under `path`: * `POST {path}/register/options` — Generate credential creation options * `POST {path}/register` — Verify registration and store credential * `POST {path}/login/options` — Generate credential request options * `POST {path}/login` — Verify authentication and issue a session (cookie via `Set-Cookie`, or `{ token }` body when `cookie: false` or the request opts in via `returnToken: true`) * `POST {path}/logout` — Revoke the session and clear the cookie. Only mounted when [`session`](#session) is `true` (the default); otherwise calls fall through as `404`. ## Usage ```ts import { Handler, Kv } from 'accounts/server' const handler = Handler.webAuthn({ kv: Kv.memory(), origin: 'https://example.com', rpId: 'example.com', }) ``` Then plug `handler` into your server framework of choice: ```ts createServer(handler.listener) // Node.js Bun.serve({ fetch: handler.fetch }) // Bun Deno.serve({ fetch: handler.fetch }) // Deno app.all('*', c => handler.fetch(c.request)) // Elysia app.use(handler.listener) // Express app.use(c => handler.fetch(c.req.raw)) // Hono export const GET = handler.fetch // Next.js export const POST = handler.fetch // Next.js ``` :::warning `Kv.memory()` is not recommended for production use. Instead, use a persistent store like Cloudflare or Vercel KV, or a Redis instance. See [`Kv`](/docs/server/kv) for available adapters. ::: ## Return Type `Handler.webAuthn` returns a [`Handler`](/docs/server/handler.compose#handler) extended with a `getSession` helper for resolving the current session from a follow-up request: ```ts type ReturnType = Handler & { getSession: (req: Request) => Promise } ``` #### `getSession` Resolves the current session from a request's session cookie or `Authorization: Bearer ` header. Returns `undefined` when no token is present, the token has been revoked, or the session has been disabled (`session: false`). ```ts import { Handler, Kv } from 'accounts/server' const handler = Handler.webAuthn({ kv: Kv.memory(), origin: 'https://example.com', rpId: 'example.com', }) // Inside another route on the same server: app.get('/me', async (req) => { const session = await handler.getSession(req) if (!session) return new Response('unauthorized', { status: 401 }) return Response.json({ credentialId: session.credentialId }) }) ``` ### `SessionPayload` ```ts type SessionPayload = { /** Credential ID returned by the authenticator. */ credentialId: string /** Credential public key (hex). */ publicKey: string /** Optional `userHandle` returned by the authenticator. */ userId?: string | undefined /** Unix timestamp (seconds) when the session was issued. */ issuedAt: number /** Unix timestamp (seconds) when the session expires. */ expiresAt: number } ``` The session payload is persisted in [`kv`](#kv) under `session:{token}` and is also the value returned by [`getSession`](#getsessionreq). ## Parameters ### cookie * **Type:** `boolean` * **Default:** `true` Whether to issue a session cookie on successful login. When `false`, the login response always contains `{ token }` in the body, no `Set-Cookie` header is sent, logout does not clear a cookie, and `getSession` ignores any incoming cookie — only `Authorization: Bearer ` is honored. Use this when the SDK lives in a non-browser context or the host app already manages its own auth cookies. ### cookieName * **Type:** `string` * **Default:** `'accounts_webauthn'` Cookie name for the session token. ```ts twoslash import { Handler, Kv } from 'accounts/server' const handler = Handler.webAuthn({ kv: Kv.memory(), origin: 'https://example.com', rpId: 'example.com', cookieName: 'my_app_session', // [!code focus] }) ``` ### kv * **Type:** [`Kv`](/docs/server/kv) * **Required** Key-value store for challenges, credentials, and sessions. [See `Kv`](/docs/server/kv) for available adapters. ```ts twoslash import { Handler, Kv } from 'accounts/server' const handler = Handler.webAuthn({ kv: Kv.memory(), // [!code focus] origin: 'https://example.com', rpId: 'example.com', }) ``` ### onAuthenticate * **Type:** `(params: { credentialId: string; publicKey: string; userId?: string; request: Request }) => Response | Promise | void | Promise` * **Optional** Called after a successful authentication, before the session token is issued. * Returning a `Response` merges its JSON body and status onto the default login response. * Returning `void` (or a `Promise`) leaves the default response untouched. * Throwing rejects the request with `401`. The thrown error's `message` is surfaced as the response `error` field, the challenge is consumed, and no session is issued. ```ts twoslash import { Handler, Kv } from 'accounts/server' const handler = Handler.webAuthn({ kv: Kv.memory(), origin: 'https://example.com', rpId: 'example.com', onAuthenticate: async ({ credentialId, publicKey, request }) => { // [!code focus] if (await isBlocked(credentialId)) throw new Error('credential blocked') // [!code focus] }, // [!code focus] }) ``` ### onRegister * **Type:** `(params: { aaguid?: string; credentialId: string; name: string; publicKey: string; request: Request }) => Response | Promise | void | Promise` * **Optional** Called after a successful registration. `aaguid` is the authenticator model identifier when reported, and `name` is the value passed to `POST /register/options`. Returning a `Response` merges its JSON body and status onto the default registration response. Throwing removes the new credential and surfaces as a `400` error response. ```ts twoslash import { Handler, Kv } from 'accounts/server' const handler = Handler.webAuthn({ kv: Kv.memory(), origin: 'https://example.com', rpId: 'example.com', onRegister: ({ aaguid, credentialId, name, publicKey }) => { // [!code focus] console.log('Registered:', name, credentialId, aaguid) // [!code focus] }, // [!code focus] }) ``` ### origin * **Type:** `string | readonly string[]` * **Required** Expected origin(s) for WebAuthn verification (e.g. `'https://example.com'`). ```ts twoslash import { Handler, Kv } from 'accounts/server' const handler = Handler.webAuthn({ kv: Kv.memory(), origin: 'https://example.com', // [!code focus] rpId: 'example.com', }) ``` ### path * **Type:** `string` * **Default:** `''` Path prefix for the WebAuthn endpoints. ```ts twoslash import { Handler, Kv } from 'accounts/server' const handler = Handler.webAuthn({ kv: Kv.memory(), origin: 'https://example.com', rpId: 'example.com', path: '/webauthn', // [!code focus] }) ``` ### rpId * **Type:** `string` * **Required** Relying Party ID (e.g. `'example.com'`). ```ts twoslash import { Handler, Kv } from 'accounts/server' const handler = Handler.webAuthn({ kv: Kv.memory(), origin: 'https://example.com', rpId: 'example.com', // [!code focus] }) ``` ### session * **Type:** `boolean` * **Default:** `true` Whether to issue a session on successful login. When `false`, login acts as a stateless WebAuthn verification — no token is generated, no entry is written to the kv, and no cookie is sent. The login response still carries `{ credentialId, publicKey, userId? }`. `getSession` always returns `undefined` and the `/logout` route is not mounted (so it returns `404`). Use this when the host application mints its own session token (e.g. a JWT inside `onAuthenticate`). ### ttl * **Type:** `{ challenge?: number; session?: number }` * **Optional** TTLs in seconds for stored challenges and issued sessions. #### ttl.challenge * **Type:** `number` * **Default:** `300` (5 minutes) Maximum age of a challenge in seconds before it expires. ```ts twoslash import { Handler, Kv } from 'accounts/server' const handler = Handler.webAuthn({ kv: Kv.memory(), origin: 'https://example.com', rpId: 'example.com', ttl: { challenge: 600 }, // [!code focus] }) ``` #### ttl.session * **Type:** `number` * **Default:** `86400` (24 hours) Lifetime of an issued session in seconds. After expiry, `getSession` returns `undefined` and the kv entry is evicted by the underlying store. ```ts twoslash import { Handler, Kv } from 'accounts/server' const handler = Handler.webAuthn({ kv: Kv.memory(), origin: 'https://example.com', rpId: 'example.com', ttl: { session: 60 * 60 * 24 * 7 }, // 7 days // [!code focus] }) ``` ### cors * **Type:** `boolean | Cors` * **Default:** `true` Inherited from [`from.Options`](/docs/server/handler.compose#optionscors). CORS configuration applied to every response. ### headers * **Type:** `Headers | Record` * **Optional** Inherited from [`from.Options`](/docs/server/handler.compose#optionsheaders). Additional headers merged onto every response. # `hc` Hono's typed RPC client, re-exported from `accounts/server` for convenience. Use it to call routes mounted by [`Handler.compose`](/docs/server/handler.compose), [`Handler.exchange`](/docs/server/handler.exchange), or any other handler that returns a typed Hono app, with full end-to-end type safety. ```ts import { hc } from 'accounts/server' // equivalent to: import { hc } from 'hono/client' ``` See [Hono's RPC documentation](https://hono.dev/docs/guides/rpc) for the complete client API. ## Usage Pass `typeof handler` as the type argument to `hc` to derive a fully-typed client from your server handler's route schema: ```ts twoslash import { Handler, hc } from 'accounts/server' const handler = Handler.exchange() const client = hc('https://wallet.example.com') const res = await client.exchange.quote.$post({ json: { amount: '1', pairToken: 'pathUSD', slippage: 0.01, token: 'USDC', type: 'sell', }, }) if (res.ok) { const { calls, pairToken, token } = await res.json() } ``` [`Handler.compose`](/docs/server/handler.compose) preserves every sub-handler's route schema on the composed app's type, so a single `hc` covers every mounted handler: ```ts twoslash // @noErrors import { Handler, hc } from 'accounts/server' const handler = Handler.compose( [Handler.exchange(), Handler.relay()], { path: '/api' }, ) const client = hc('https://wallet.example.com') await client.api.exchange.quote.$post({ json: { /* ... */ } }) ``` ## Parameters ### baseUrl * **Type:** `string` * **Required** Base URL of the server hosting the handler. ### options * **Type:** `ClientRequestOptions` * **Optional** Hono client options — `headers`, `init` (passed to `fetch`), and a custom `fetch` implementation. See [Hono's RPC documentation](https://hono.dev/docs/guides/rpc#init-option) for the full shape. ```ts twoslash import { Handler, hc } from 'accounts/server' const handler = Handler.exchange() const client = hc('https://wallet.example.com', { headers: { authorization: 'Bearer ...' }, // [!code focus] }) ``` # `Identity.verify` Verifies a wallet-issued identity token (JWT) against an issuer's JWKS and returns its claims. Use it when you handle the sign-in round-trip yourself (for example, minting your own session in [`onAuthenticate`](/docs/server/handler.auth#onauthenticate)) instead of letting the [`Handler.auth({ identity })`](/docs/server/handler.auth#identity) option fold the email onto the session. It asserts the signature, `iss`, `aud`, and `exp`, plus `email_verified === true`. Pass `subject` and/or `nonce` to additionally cross-check the token's `sub` (account address) and `nonce`. Throws on any failure. ```ts twoslash // @noErrors import { Identity } from 'accounts/server' const claims = await Identity.verify(idToken, { audience: 'https://app.example.com', // your app origin (token `aud`) issuer: 'https://wallet.tempo.xyz/api/oidc', // the wallet's identity issuer subject: address, // the signed-in account address }) claims.email // verified email ``` ## Parameters ```ts type Options = { /** Expected audience (`aud`) — your app's origin. */ audience: string /** Issuer (IdP) URL whose JWKS signs the token. */ issuer: string /** When set, require the token's `nonce` to equal this value. */ nonce?: string | undefined /** When set, require the token's `sub` to equal this address (case-insensitive). */ subject?: string | undefined } ``` ## Return Type ```ts type Claims = { /** Verified email, if present. */ email: string | undefined /** OIDC nonce echoed in the token, if present. */ nonce: string | undefined /** Token subject (`sub`) — the account address. */ subject: string } ``` # `Kv` Minimal key-value store interface used by the SDK's server primitives (e.g. SIWE nonce store, WebAuthn session store, CLI device-code store). Values are JSON-serialized when stored. TTLs are optional; consumers that need expiry pass `{ ttl }` (in seconds) to `set` and the implementation lazily evicts (memory) or relies on the backing store's native expiry (Cloudflare KV). ## Usage ```ts import { Kv } from 'accounts/server' // In-memory (for development/testing). const kv = Kv.memory() // Cloudflare Workers KV (eventually consistent — no atomic `take` or `create`). const kv = Kv.cloudflare(env.MY_KV_NAMESPACE) // Cloudflare Durable Object (linearizable — atomic `take` and `create` available). const kv = Kv.durableObject(env.NONCE_DO) ``` ## Adapters ### Kv.memory(options?) Creates an in-memory key-value store backed by a `Map`. Lazily evicts expired entries on read/write. Useful for development and testing, and for single-process deployments. ```ts const kv = Kv.memory() const kv = Kv.memory({ now: () => fakeClock.now }) ``` #### memory.Options ```ts type memory.Options = { /** Clock function for TTL accounting. Defaults to `Date.now`. */ now?: (() => number) | undefined } ``` ### Kv.cloudflare(namespace) Creates a key-value store backed by a Cloudflare Workers KV namespace (or compatible binding). Uses the underlying store's native `expirationTtl` for TTL — Cloudflare KV's minimum TTL is 60 seconds, enforced by the platform independent of what's passed here. **Not safe for one-time-consume or unique-key semantics.** Cloudflare KV is eventually consistent across data centers — concurrent read+delete races can let the same key be "consumed" twice, and concurrent create-if-absent checks can race. The optional [`take`](#take-key-optional) and [`create`](#create-key-value-options-optional) methods are intentionally NOT implemented. Use [`Kv.durableObject`](#kv-durableobject-namespace-options) (or another linearizable backend) when strict atomicity is required. * **namespace** — Cloudflare KV namespace binding. ### Kv.durableObject(namespace, options?) Adapts a Cloudflare Durable Object namespace into a `Kv` with atomic [`take`](#take-key-optional) and [`create`](#create-key-value-options-optional). Unlike `Kv.cloudflare`, a Durable Object's storage is single-actor and linearizable — `take` (read+delete) and `create` (create-if-absent) are guaranteed atomic across concurrent callers, which makes this the recommended backend for SIWE challenge nonce storage on Cloudflare Workers. Pair with [`NonceStorage`](#noncestorage) (or your own DO class implementing the same fetch protocol). ```ts // wrangler.jsonc // { // "durable_objects": { // "bindings": [{ "name": "NONCE_DO", "class_name": "NonceStorage" }] // }, // "migrations": [{ "tag": "v1", "new_classes": ["NonceStorage"] }] // } // worker.ts import { Handler, Kv, NonceStorage } from 'accounts/server' export { NonceStorage } export default { fetch(req, env) { const handler = Handler.auth({ store: Kv.durableObject(env.NONCE_DO), origin: 'https://app.example.com', }) return handler.fetch(req) }, } ``` #### durableObject.Namespace Minimal shape of a Cloudflare Durable Object namespace binding. Compatible with `DurableObjectNamespace` from `@cloudflare/workers-types`. ```ts type durableObject.Namespace = { idFromName: (name: string) => unknown get: (id: unknown) => { fetch: (input: string, init?: unknown) => Promise } } ``` #### durableObject.Options ```ts type durableObject.Options = { /** * Durable Object instance name. Defaults to `'default'` (a single * shared actor). Use a per-tenant name if you need isolation. */ name?: string | undefined } ``` ### Kv.from(kv) Wraps an existing `Kv`-shaped object so the SDK accepts it as a `Kv`. Useful when adapting a custom backend. ```ts const kv = Kv.from({ async get(key) { /* ... */ }, async set(key, value, options) { /* ... */ }, async delete(key) { /* ... */ }, async take(key) { /* ... */ }, // optional }) ``` ## NonceStorage Reference Durable Object class implementing the [`Kv.durableObject`](#kv-durableobject-namespace-options) fetch protocol. Export from your Worker entry and bind it under `class_name: "NonceStorage"` in `wrangler.jsonc`. The class is framework-agnostic — it doesn't import `cloudflare:workers` so it works with both the legacy DO API (`fetch(req)` only) and the newer `extends DurableObject` API. ```ts import { NonceStorage } from 'accounts/server' export { NonceStorage } ``` ### NonceStorage.State Subset of `DurableObjectState` actually used by `NonceStorage`. ```ts type NonceStorage.State = { storage: { get: (key: string) => Promise put: (key: string, value: unknown) => Promise delete: (key: string) => Promise } } ``` ### NonceStorage.Entry Internal storage shape: value plus optional absolute expiry timestamp (ms). ```ts type NonceStorage.Entry = { value: unknown; expiresAt?: number } ``` ## Interface ```ts type Kv = { /** Read a value by key. Returns `undefined` when missing or expired. */ get: (key: string) => Promise /** Write a value. When `ttl` is set, the entry expires after the given duration in seconds. */ set: (key: string, value: unknown, options?: set.Options) => Promise /** Delete a value by key. */ delete: (key: string) => Promise /** * Atomic create-if-absent. Optional — see below. */ create?: (key: string, value: unknown, options?: set.Options) => Promise /** * Atomic read-and-delete. Optional — see below. */ take?: (key: string) => Promise } type set.Options = { /** Time-to-live in seconds. After this duration, `get` returns `undefined`. */ ttl?: number | undefined } ``` ### `create(key, value, options?)` (optional) Atomic create-if-absent. Returns `true` when the value was written, or `false` when a non-expired value already exists. Backends with a linearizable create primitive should implement this so consumers can reject duplicates atomically. Consumers may fall back to `get` then `set` when strict atomicity is not required. | Adapter | `create` implemented? | | ------------------ | ---------------------------------------------------------------------------- | | `Kv.memory` | ✅ atomic in-process | | `Kv.durableObject` | ✅ linearizable | | `Kv.cloudflare` | ❌ (eventually consistent — see [`Kv.cloudflare`](#kv-cloudflare-namespace)) | ### `take(key)` (optional) Atomic read-and-delete. Returns the value if present, `undefined` if missing or expired. Across concurrent callers, exactly one observer receives a non-`undefined` return for a given key, and the key is removed exactly once. Required for one-time-consume semantics (e.g. SIWE challenge nonces). Backends without a linearizable read+delete primitive (e.g. eventually-consistent stores like Cloudflare KV) should leave this undefined; the consuming handler will refuse to accept the store at construction time and fall back to a different backend (e.g. a Durable Object). | Adapter | `take` implemented? | | ------------------ | ---------------------------------------------------------------------------- | | `Kv.memory` | ✅ atomic in-process | | `Kv.durableObject` | ✅ linearizable | | `Kv.cloudflare` | ❌ (eventually consistent — see [`Kv.cloudflare`](#kv-cloudflare-namespace)) | # `Kv.cloudflare` Adapts a Cloudflare Workers KV namespace (or compatible binding) into a [`Kv`](/docs/server/kv). Uses the underlying store's native `expirationTtl` for TTL — Cloudflare KV's minimum TTL is 60 seconds, enforced by the platform independent of what's passed here. :::warning **Not safe for one-time-consume or unique-key semantics.** Cloudflare KV is eventually consistent across data centers — concurrent read+delete races can let the same key be "consumed" twice, and concurrent create-if-absent checks can race. The optional [`take`](/docs/server/kv#take-key-optional) and [`create`](/docs/server/kv#create-key-value-options-optional) methods are intentionally NOT implemented. Use [`Kv.durableObject`](/docs/server/kv.durableObject) (or another linearizable backend) when strict atomicity is required. ::: ## Usage ```ts import { Kv } from 'accounts/server' const kv = Kv.cloudflare(env.MY_KV_NAMESPACE) ``` Wire the binding in `wrangler.jsonc`: ```jsonc { "kv_namespaces": [{ "binding": "MY_KV_NAMESPACE", "id": "" }] } ``` ## Parameters ### namespace * **Type:** `Kv.cloudflare.Parameters` * **Required** A Cloudflare Workers KV namespace binding (or anything matching the same shape): ```ts type Parameters = { get: (key: string, format: 'json') => Promise put: ( key: string, value: string, options?: { expirationTtl?: number } | undefined, ) => Promise delete: (key: string) => Promise } ``` ```ts import { Handler, Kv } from 'accounts/server' export default { fetch(req: Request, env: { KV: KVNamespace }) { const handler = Handler.exchange({ kv: Kv.cloudflare(env.KV), // [!code focus] }) return handler.fetch(req) }, } ``` ## Return Type A [`Kv`](/docs/server/kv#interface) instance with `get`, `set`, and `delete`. `take` and `create` are intentionally omitted — handlers that require strict atomicity should be paired with [`Kv.durableObject`](/docs/server/kv.durableObject) instead. # `Kv.durableObject` Adapts a Cloudflare Durable Object namespace into a [`Kv`](/docs/server/kv) with atomic [`take`](/docs/server/kv#take-key-optional) and [`create`](/docs/server/kv#create-key-value-options-optional). Unlike [`Kv.cloudflare`](/docs/server/kv.cloudflare), a Durable Object's storage is single-actor and linearizable — `take` (read+delete) and `create` (create-if-absent) are guaranteed atomic across concurrent callers, which makes this the recommended backend for SIWE challenge nonce storage on Cloudflare Workers. Pair with [`NonceStorage`](/docs/server/kv#noncestorage) (or your own DO class implementing the same fetch protocol). ## Usage ```ts import { Handler, Kv, NonceStorage } from 'accounts/server' export { NonceStorage } export default { fetch(req: Request, env: { NONCE_DO: DurableObjectNamespace }) { const handler = Handler.auth({ origin: 'https://app.example.com', store: Kv.durableObject(env.NONCE_DO), }) return handler.fetch(req) }, } ``` Wire the Durable Object class in `wrangler.jsonc`: ```jsonc { "durable_objects": { "bindings": [{ "name": "NONCE_DO", "class_name": "NonceStorage" }] }, "migrations": [{ "tag": "v1", "new_classes": ["NonceStorage"] }] } ``` ## Parameters ### namespace * **Type:** `Kv.durableObject.Namespace` * **Required** A Cloudflare Durable Object namespace binding (or anything matching the same shape): ```ts type Namespace = { idFromName: (name: string) => unknown get: (id: unknown) => { fetch: (input: string, init?: unknown) => Promise } } ``` Compatible with `DurableObjectNamespace` from `@cloudflare/workers-types`. ### options * **Type:** `Kv.durableObject.Options` * **Default:** `{}` #### options.name * **Type:** `string` * **Default:** `'default'` Durable Object instance name. Defaults to a single shared actor. Use a per-tenant name when you need isolation between callers. ```ts import { Handler, Kv } from 'accounts/server' const handler = Handler.auth({ origin: 'https://app.example.com', store: Kv.durableObject(env.NONCE_DO, { name: `tenant:${tenantId}`, // [!code focus] }), }) ``` ## Return Type A [`Kv`](/docs/server/kv#interface) instance with `get`, `set`, `delete`, and the linearizable `take` and `create`. # `Kv.from` Wraps an existing [`Kv`](/docs/server/kv)-shaped object so the SDK accepts it as a `Kv`. Useful when adapting a custom backend (Redis, DynamoDB, Postgres, an internal cache) — implement the four-method interface and pass the result through `Kv.from`. `Kv.from` is a structural pass-through — it returns the input unchanged. It exists so callers can opt into the `Kv` type without an explicit `as` cast and so future evolutions of the interface stay compatible. ## Usage ```ts twoslash import { Kv } from 'accounts/server' declare const redis: { get: (key: string) => Promise set: (key: string, value: string, mode?: 'EX', ttl?: number) => Promise del: (key: string) => Promise } // ---cut--- const kv = Kv.from({ async get(key) { const value = await redis.get(key) return value ? JSON.parse(value) : undefined }, async set(key, value, options) { if (options?.ttl) await redis.set(key, JSON.stringify(value), 'EX', options.ttl) else await redis.set(key, JSON.stringify(value)) }, async delete(key) { await redis.del(key) }, }) ``` ## Parameters ### kv * **Type:** [`Kv`](/docs/server/kv#interface) * **Required** A `Kv`-shaped object. Must implement `get`, `set`, and `delete`. Optionally implement [`take`](/docs/server/kv#take-key-optional) for atomic read-and-delete semantics — handlers that require it (e.g. [`Handler.auth`](/docs/server/handler.auth) on multi-instance deployments) refuse stores without it. ```ts twoslash import { Kv } from 'accounts/server' const kv = Kv.from({ async get(_key) { /* ... */ return undefined }, async set(_key, _value, _options) { /* ... */ }, async delete(_key) { /* ... */ }, async take(_key) { /* ... */ return undefined }, // [!code focus] }) ``` ## Return Type The same `Kv` instance passed in, typed as `Kv`. # `Kv.memory` Creates an in-memory [`Kv`](/docs/server/kv) backed by a `Map`. Lazily evicts expired entries on read/write. Useful for development, tests, and single-process deployments where cross-instance consistency is not required. `Kv.memory` implements the optional [`take`](/docs/server/kv#take-key-optional) primitive — `take` is atomic in-process because the synchronous `Map.get` + `Map.delete` runs in a single microtask, so concurrent callers within the same Node/Bun/Worker process cannot both observe the value. :::warning Not suitable for multi-instance production deployments. State is per-process, so issued sessions and one-time challenge nonces will not be visible to other instances. Use [`Kv.cloudflare`](/docs/server/kv.cloudflare) or [`Kv.durableObject`](/docs/server/kv.durableObject) instead. ::: ## Usage ```ts twoslash import { Kv } from 'accounts/server' const kv = Kv.memory() ``` ## Parameters ### options * **Type:** `Kv.memory.Options` * **Default:** `{}` #### options.now * **Type:** `() => number` * **Default:** `Date.now` Clock function used for TTL accounting. Override in tests to make TTL evaluation deterministic. ```ts twoslash import { Kv } from 'accounts/server' let clock = 0 const kv = Kv.memory({ now: () => clock, // [!code focus] }) ``` ## Return Type A [`Kv`](/docs/server/kv#interface) instance with `get`, `set`, `delete`, and the optional `take`. # `Keystore` Pluggable backends for key material. A keystore owns a key end to end: `createKey()` provisions it and returns an opaque handle, and `toAccount()` turns a persisted record back into a signing account. The [`Provider`](/docs/api/provider#accesskeykeystores) uses keystores to back access keys, one keystore per key type. Keystores hold key material; [`Storage`](/docs/api/storage) persists provider state. A keystore can be local (WebCrypto, Secure Enclave / Android Keystore) or remote (an HSM or key-management service). ## Keystores ## Interface Apps with custom needs (own native module, HSM, remote signer) implement the two-function interface: ```ts type Handle = { kind: string; [key: string]: unknown } type Keystore = { /** * Whether handles hold live objects (e.g. a CryptoKey) that persist only * through structured-clone storage — on other storage the handle is * stripped at persist time, making the key session-only. When false * (default), handles must survive JSON serialization, portable across * all storage adapters. */ requiresStructuredClone?: boolean /** * Creates access-key material. `handle` is opaque and persisted verbatim * by the SDK. Must fail loudly when runtime prerequisites are missing * (e.g. no Secure Enclave, no `crypto.subtle`) so provisioning errors * surface at authorization time, not at first sign. */ createKey(): Promise<{ publicKey: Hex; handle: Handle }> /** * Turns a persisted key record back into a signing account. * Called once per launch; the SDK caches the result per record. */ toAccount( record: { keyType: string; publicKey: Hex; handle: Handle }, context: { access: Address; keyAuthorizationManager: KeyAuthorizationManager }, ): Promise | Account } ``` ### Handles The handle is opaque to the SDK: it is persisted verbatim alongside the access-key record (so material and record share one lifecycle — one write, one backup, one removal) and handed back to `toAccount` on hydration. The `kind` discriminant marks which keystore wrote it; the remaining fields are owned by that keystore and must survive its declared persistence format. ### Errors `toAccount` errors drive the record lifecycle: * Throw `Keystore.KeyUnavailableError` when the key behind the handle is **permanently gone** (e.g. hardware key deleted, app keychain wiped). The SDK evicts the record so callers fall back to authorizing a fresh key. * Throw any other error for handles the keystore does not recognize or **transient** failures (e.g. device locked): the record is kept and retried on next use. A keystore composing multiple backends — say, hardware keys with a software fallback — should treat `KeyUnavailableError` as an ownership claim and not route the handle to another backend, and should gate provisioning fallback on a capability check rather than on errors, so a transient failure never silently downgrades to a weaker backend. # `Keystore.p256` Creates a pure-JS P-256 [`Keystore`](/docs/api/keystore). The private key is held as hex in the handle, so at-rest protection is the storage adapter and signing runs in JavaScript. Prefer [`Keystore.webCryptoP256`](/docs/api/keystore.webCryptoP256) or a hardware keystore where a WebCrypto implementation is available; this exists chiefly as an adapter default for environments without one. ## Usage ```ts twoslash import { Keystore, Provider } from 'accounts' const provider = Provider.create({ accessKey: { keystores: { p256: Keystore.p256() }, // [!code focus] }, }) ``` # `Keystore.secp256k1` Creates a pure-JS secp256k1 [`Keystore`](/docs/api/keystore). The private key is held as hex in the handle, so at-rest protection is the storage adapter and signing runs in JavaScript. Access keys default to p256, so secp256k1 is used only when requested explicitly via `keyType: 'secp256k1'` — useful because secp256k1 signatures are the chain's cheapest envelope. ## Usage ```ts twoslash import { Keystore, Provider } from 'accounts' const provider = Provider.create({ accessKey: { keystores: { secp256k1: Keystore.secp256k1() }, // [!code focus] }, }) ``` # `Keystore.webCryptoP256` Creates a WebCrypto P-256 [`Keystore`](/docs/api/keystore). This is the built-in default (`Keystore.defaults`). By default the key is generated non-extractable: the private key never has a JS-visible encoding, and the handle holds the live `CryptoKey`, which persists only through structured-clone storage ([`Storage.idb`](/docs/api/storage.idb), [`Storage.memory`](/docs/api/storage.memory)) — on string-based storage the key is session-only. ## Usage ```ts twoslash import { Keystore, Provider } from 'accounts' const provider = Provider.create({ accessKey: { keystores: { p256: Keystore.webCryptoP256() }, // [!code focus] }, }) ``` ## Parameters ### options.extractable * **Type:** `boolean` * **Default:** `false` Generate the key extractable and persist it as a JWK so it survives string-based storage adapters. Use this for environments without structured-clone storage where keys must survive restarts — React Native (with a WebCrypto implementation such as `react-native-quick-crypto`), Node, CLI. The private key is exported once at creation and re-imported non-extractable each session, so at-rest protection is the host app's storage adapter (pair with [`secureMmkv`](/docs/api/storage.secureMmkv) on React Native). In browsers with IndexedDB storage, prefer the non-extractable default — it keeps the key without any JS-visible encoding, at rest or at runtime. ```ts twoslash import { Keystore, Provider } from 'accounts' const provider = Provider.create({ accessKey: { keystores: { p256: Keystore.webCryptoP256({ extractable: true, // [!code focus] }), }, }, }) ``` # `Storage` Pluggable storage adapters for persisting [`Provider`](/docs/api/provider) state (connected accounts, session keys, etc.). Pass a `Storage` to `Provider.create({ storage })` to control where state is read from and written to. The default is `Storage.idb()` in the browser and `Storage.memory()` otherwise. ## Adapters ## React Native React Native adapters ship as optional subpaths so apps only install the native dependencies they use. See the [React Native guide](/docs/guides/react-native) for platform setup. ## Comparison | Adapter | Persistence | Environment | Notes | | -------------------------------------------------------- | ----------- | ------------ | -------------------------------------------------------------------------------------------------------- | | [`Storage.idb`](/docs/api/storage.idb) | Persistent | Browser | Default in the browser. Stores raw values (no JSON serialization). | | [`Storage.localStorage`](/docs/api/storage.localStorage) | Persistent | Browser | Synchronous; values are JSON-serialized. | | [`Storage.cookie`](/docs/api/storage.cookie) | Persistent | Browser | Uses `SameSite=None; Secure`. Deep objects are flattened to stay within the 4 KB-per-cookie limit. | | [`Storage.memory`](/docs/api/storage.memory) | Ephemeral | Any | Default outside the browser. Useful for SSR and tests. | | [`asyncStorage`](/docs/api/storage.asyncStorage) | Persistent | React Native | Plain-text AsyncStorage; for non-sensitive state. | | [`secureMmkv`](/docs/api/storage.secureMmkv) | Persistent | React Native | AES-256-encrypted MMKV with the key in the Keychain/Keystore; use when the provider manages access keys. | | [`Storage.combine`](/docs/api/storage.combine) | — | Any | Reads return the first non-null result; writes propagate to every storage. | | [`Storage.from`](/docs/api/storage.from) | — | Any | Wraps a custom implementation, optionally scoping all keys under a prefix. | # `Storage.combine` Combines multiple [`Storage`](/docs/api/storage) adapters into one. Reads return the first non-null result; writes propagate to every storage. Failures during reads or writes are isolated via `Promise.allSettled` so a single failing backend will not break the others. A common use case is layering an in-memory cache in front of a persistent backend, or mirroring writes between cookies and `localStorage` so server-rendered code can see what the client wrote. ## Usage ```ts twoslash import { Provider, Storage } from 'accounts' const provider = Provider.create({ storage: Storage.combine(Storage.cookie(), Storage.localStorage()), }) ``` ## Parameters ### storages * **Type:** `readonly Storage[]` * **Required** The storage adapters to combine, in priority order. The first storage that returns a non-null value for `getItem` wins. `setItem` and `removeItem` are dispatched to every storage in parallel. ```ts twoslash import { Provider, Storage } from 'accounts' const provider = Provider.create({ storage: Storage.combine( // [!code focus] Storage.memory(), // [!code focus] Storage.idb(), // [!code focus] ), // [!code focus] }) ``` # `Storage.cookie` Creates a [`Storage`](/docs/api/storage) adapter backed by `document.cookie`. Cookies are written with `SameSite=None; Secure` and a one-year `max-age`. Deep objects are flattened into individual cookies (one per leaf value) to stay within the 4 KB-per-cookie browser limit; arrays additionally store a sentinel `__length` cookie so they can be reconstructed on read. Useful for sharing provider state with a server (e.g. SSR) since cookies are sent on every request to the same origin. ## Usage ```ts twoslash import { Provider, Storage } from 'accounts' const provider = Provider.create({ storage: Storage.cookie(), }) ``` ## Parameters ### options.key * **Type:** `string` * **Default:** `'tempo'` Key prefix applied to every stored cookie name. Set this when you need to scope the cookies to a specific app, or when multiple SDK instances share the same origin. ```ts twoslash import { Provider, Storage } from 'accounts' const provider = Provider.create({ storage: Storage.cookie({ key: 'my-app', // [!code focus] }), }) ``` # `Storage.from` Creates a [`Storage`](/docs/api/storage) adapter from a custom implementation, optionally scoping all keys under a prefix. All built-in storage adapters ([`.cookie`](/docs/api/storage.cookie), [`.idb`](/docs/api/storage.idb), [`.localStorage`](/docs/api/storage.localStorage), [`.memory`](/docs/api/storage.memory)) are built on top of `Storage.from` and share its `key` prefix option. ## Usage ```ts twoslash import { Json } from 'ox' import { Provider, Storage } from 'accounts' const storage = Storage.from({ getItem(name) { return Json.parse(globalThis.sessionStorage.getItem(name) ?? 'null') }, setItem(name, value) { globalThis.sessionStorage.setItem(name, Json.stringify(value)) }, removeItem(name) { globalThis.sessionStorage.removeItem(name) }, }) const provider = Provider.create({ storage }) ``` ## Parameters ### storage * **Type:** `Storage` * **Required** The underlying storage implementation to wrap. Must provide `getItem`, `setItem`, and `removeItem`. Each method may return synchronously or as a `Promise`. Use `Json` from `ox` for string-backed storage so values such as access-key spending limits round-trip with `bigint` fields. ```ts twoslash import { Json } from 'ox' import { Storage } from 'accounts' const storage = Storage.from({ getItem(name) { // [!code focus] return Json.parse(globalThis.sessionStorage.getItem(name) ?? 'null') // [!code focus] }, // [!code focus] setItem(name, value) { // [!code focus] globalThis.sessionStorage.setItem(name, Json.stringify(value)) // [!code focus] }, // [!code focus] removeItem(name) { // [!code focus] globalThis.sessionStorage.removeItem(name) // [!code focus] }, // [!code focus] }) ``` ### options.key * **Type:** `string` * **Default:** `'tempo'` Key prefix applied to every name passed through to the underlying storage. For example, with the default key, `getItem('foo')` reads from `'tempo.foo'`. ```ts twoslash import { Json } from 'ox' import { Storage } from 'accounts' const storage = Storage.from( { getItem(name) { return Json.parse(globalThis.sessionStorage.getItem(name) ?? 'null') }, setItem(name, value) { globalThis.sessionStorage.setItem(name, Json.stringify(value)) }, removeItem(name) { globalThis.sessionStorage.removeItem(name) }, }, { key: 'my-app', // [!code focus] }, ) ``` # `Storage.idb` Creates a [`Storage`](/docs/api/storage) adapter backed by IndexedDB (via [`idb-keyval`](https://github.com/jakearchibald/idb-keyval)). Values are stored raw — no JSON serialization is performed, so structured-clone-compatible values like `Map`, `Set`, `Date`, and `ArrayBuffer` round-trip natively. This is the default storage for [`Provider.create`](/docs/api/provider) in the browser. ## Usage ```ts twoslash import { Provider, Storage } from 'accounts' const provider = Provider.create({ storage: Storage.idb(), }) ``` ## Parameters ### options.key * **Type:** `string` * **Default:** `'tempo'` Key prefix applied to every entry written to the IndexedDB store. Useful when several apps share the same origin and you want to avoid key collisions. ```ts twoslash import { Provider, Storage } from 'accounts' const provider = Provider.create({ storage: Storage.idb({ key: 'my-app', // [!code focus] }), }) ``` # `Storage.localStorage` Creates a [`Storage`](/docs/api/storage) adapter backed by `globalThis.localStorage`. Values are JSON-serialized on write and parsed on read. Reads return `null` when the underlying value is missing or fails to parse. ## Usage ```ts twoslash import { Provider, Storage } from 'accounts' const provider = Provider.create({ storage: Storage.localStorage(), }) ``` ## Parameters ### options.key * **Type:** `string` * **Default:** `'tempo'` Key prefix applied to every entry written to `localStorage`. Useful when several apps share the same origin and you want to avoid key collisions. ```ts twoslash import { Provider, Storage } from 'accounts' const provider = Provider.create({ storage: Storage.localStorage({ key: 'my-app', // [!code focus] }), }) ``` # `Storage.memory` Creates an in-memory [`Storage`](/docs/api/storage) adapter backed by a `Map`. Values are kept for the lifetime of the process and discarded when it exits. Useful for SSR, tests, and any non-browser environment where persistent storage is unavailable or undesirable. This is the default storage for [`Provider.create`](/docs/api/provider) outside the browser. ## Usage ```ts twoslash import { Provider, Storage } from 'accounts' const provider = Provider.create({ storage: Storage.memory(), }) ``` ## Parameters ### options.key * **Type:** `string` * **Default:** `'tempo'` Key prefix applied to every entry. Useful when multiple SDK instances share the same in-memory map (uncommon, but supported for parity with the persistent adapters). ```ts twoslash import { Provider, Storage } from 'accounts' const provider = Provider.create({ storage: Storage.memory({ key: 'my-app', // [!code focus] }), }) ``` # `asyncStorage` Creates a [`Storage`](/docs/api/storage) adapter backed by [`@react-native-async-storage/async-storage`](https://react-native-async-storage.github.io/async-storage/). Values are JSON-serialized and stored in plain text. Use it for non-sensitive provider state; when the provider manages access keys, use [`secureMmkv`](/docs/api/storage.secureMmkv) instead. The adapter ships as an optional subpath — `accounts/react-native/async-storage` — so apps that do not use it do not install AsyncStorage: ```sh npx expo install @react-native-async-storage/async-storage ``` ## Usage ```ts twoslash import { Provider } from 'accounts' import { asyncStorage } from 'accounts/react-native/async-storage' const provider = Provider.create({ storage: asyncStorage(), }) ``` ## Parameters ### options.key * **Type:** `string` * **Default:** `'tempo'` Key prefix applied to every entry written to AsyncStorage. Useful to avoid collisions with other AsyncStorage usage in the app. ```ts twoslash import { Provider } from 'accounts' import { asyncStorage } from 'accounts/react-native/async-storage' const provider = Provider.create({ storage: asyncStorage({ key: 'my-app', // [!code focus] }), }) ``` # `secureMmkv` Creates a [`Storage`](/docs/api/storage) adapter backed by an AES-256-encrypted [`react-native-mmkv`](https://github.com/mrousavy/react-native-mmkv) instance. Provider state can outgrow the device Keychain/Keystore (iOS limits SecureStore items to a few kilobytes), so `secureMmkv` stores the full snapshot in encrypted MMKV and keeps only the 32-byte encryption key in the Keychain/Keystore. Use it whenever the provider manages access keys. The adapter ships as an optional subpath — `accounts/react-native/secure-mmkv` — so apps that do not use it do not install MMKV: ```sh npx expo install react-native-mmkv react-native-nitro-modules expo-secure-store ``` ## Usage ```ts twoslash import { Provider } from 'accounts' import { secureMmkv } from 'accounts/react-native/secure-mmkv' const provider = Provider.create({ storage: secureMmkv(), }) ``` ## Encryption Key On first use the adapter loads its MMKV encryption key from Expo SecureStore, creating and persisting one when none exists. The key is stored with `AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY`: it is readable by background work once the device has been unlocked, but it never leaves the device — state does not restore onto a new device from a backup. The key helper is exported from `accounts/react-native/expo-secure-store` for apps that need a custom SecureStore item name or options: ```ts twoslash import { getOrCreateMmkvEncryptionKey } from 'accounts/react-native/expo-secure-store' import { secureMmkv } from 'accounts/react-native/secure-mmkv' const storage = secureMmkv({ encryptionKey: await getOrCreateMmkvEncryptionKey({ name: 'my-app.mmkvEncryptionKey', store: { keychainService: 'my-app' }, }), }) ``` Create one `secureMmkv()` instance per MMKV `id`. Two instances racing to create the key on first launch can each persist a different key, leaving one instance's data unreadable on the next launch. ## Parameters ### options.key * **Type:** `string` * **Default:** `'tempo'` Key prefix applied to every entry written to the MMKV instance. ### options.id * **Type:** `string` * **Default:** `'tempo-secure'` MMKV instance id. Use distinct ids to keep multiple MMKV instances separate. ### options.encryptionKey * **Type:** `string` * **Default:** a key loaded from Expo SecureStore MMKV encryption key. Pass one to manage the key yourself — the adapter then never touches SecureStore. Keys can be at most 16 bytes with `AES-128` encryption and 32 bytes with `AES-256`. ### options.encryptionType * **Type:** `'AES-128' | 'AES-256'` * **Default:** `'AES-256'` MMKV encryption algorithm. ### options.path * **Type:** `string` * **Optional** MMKV root path. By default MMKV stores files inside the app's documents directory. ### options.mode * **Type:** `'single-process' | 'multi-process'` * **Default:** `'single-process'` MMKV process mode. Use `multi-process` when the instance is shared with app extensions or background services. ### options.readOnly * **Type:** `boolean` * **Default:** `false` Open the MMKV instance read-only. ### options.compareBeforeSet * **Type:** `boolean` * **Default:** `false` Skip the file write when the new value equals what is already stored. ## getOrCreateMmkvEncryptionKey Loads or creates the SecureStore-persisted encryption key used by `secureMmkv`. Returns the existing key when one is stored, otherwise generates a random 32-byte key and persists it. ### options.name * **Type:** `string` * **Default:** `'tempo.mmkvEncryptionKey'` SecureStore item name the key is stored under. ### options.store * **Type:** `SecureStore.SecureStoreOptions` * **Default:** `{ keychainAccessible: AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY }` SecureStore options used for both reading and writing the key. # Secp256k1 Adapter The `secp256k1` adapter signs in-process with a `secp256k1` private key. Use it when you have a key the host environment owns (server-side signers, fee payers, scripts) or when you need a quick development signer that persists a generated key in storage. Use it when you want: * A pinned private key supplied by your environment (e.g. server-side signers, scripted flows). * A development-only signer that generates and persists a random key in the provider's storage. * A lightweight signing layer without passkey or hosted wallet UX. :::warning Storing keys in browser storage in plaintext is dangerous — only use the unpinned form for development, testing, or when the threat model allows it. ::: ## Configure ```ts twoslash import { Provider, secp256k1 } from 'accounts' const provider = Provider.create({ adapter: secp256k1({ privateKey: '0x...' }), }) ``` ## Human Pass Needed This page should be expanded with key-rotation guidance, server-side deployment patterns, and storage warnings for browser environments. ## Next Steps * [Custom adapter](/docs/adapters/custom) * [`secp256k1` API reference](/docs/api/secp256k1) * [`local` API reference](/docs/api/local) # Bring Your Auth Bring your own authentication and signing infrastructure, then expose it to app code through a Tempo Accounts SDK adapter. This path is for teams that already have user identity, compliance, custody, or signing requirements that should remain outside Tempo Wallet. ## Common patterns | Pattern | Use it when | | --------------------------------------------------- | ------------------------------------------------------- | | [Privy](/docs/enterprise/bring-your-auth/privy) | Privy owns app auth and wallet state. | | [AWS KMS](/docs/enterprise/bring-your-auth/aws-kms) | Keys live in AWS-controlled infrastructure. | | [Turnkey](/docs/enterprise/bring-your-auth/turnkey) | Turnkey owns signer infrastructure. | | [Custom](/docs/enterprise/bring-your-auth/custom) | Your own service owns auth, signing, or wallet hosting. | ## Integration shape ```mermaid flowchart LR App["App"] SDK["Tempo Accounts SDK"] Adapter["Custom adapter"] Auth["Auth/session system"] Signer["Signer infrastructure"] Tempo["Tempo"] App --> SDK --> Adapter Adapter --> Auth Adapter --> Signer Signer --> Tempo ``` ## Human Pass Needed This section needs enterprise positioning and security review before publishing as final guidance. ## Next Steps * [Custom adapter](/docs/adapters/custom) * [Hosted Universal Wallets](/docs/enterprise/hosted-universal-wallets) * [Spend Permissions](/docs/guides/spend-permissions) # Hosted Universal Wallets Host a universal wallet on your own domain, then expose it to apps through a custom adapter. Use this path when you want the portability of a wallet surface but need to own the domain, brand, auth, or signer infrastructure. ## Architecture ```mermaid flowchart LR App["Partner app"] SDK["Tempo Accounts SDK"] Adapter["Hosted wallet adapter"] Wallet["Wallet domain"] Signer["Signer service"] App --> SDK --> Adapter --> Wallet --> Signer ``` ## Human Pass Needed This page needs product and security input before implementation. The final version should define domain trust, iframe or popup behavior, account portability, and operational responsibilities. ## Next Steps * [Custom adapter](/docs/adapters/custom) * [Bring Your Auth](/docs/enterprise/bring-your-auth) * [Deploying to Production](/docs/production) # `Handler.codeAuth` (Deprecated) **Deprecated.** Use [`Handler.deviceCode`](/docs/server/handler.deviceCode) for new integrations. `Handler.codeAuth` remains available while existing CLI consumers migrate to `/auth/device`. Creates a generic device-code handler used to bootstrap access keys for non-browser SDK contexts (CLIs, scripts). Mounts four endpoints under `path`: * `POST {path}/code` — create a pending device code from an SDK request * `GET {path}/pending/:code` — return the pending request bound to a code, for the approval UI * `POST {path}/poll/:code` — poll until the device code is authorized, then exchange for the granted access key * `POST {path}` — authorize a pending device code with a signed key authorization Each endpoint applies the configured [`rateLimit`](#ratelimit) and reads JSON bodies up to [`maxBodyBytes`](#maxbodybytes). ## Usage ```ts import { Handler, CliAuth } from 'accounts/server' const handler = Handler.codeAuth({ store: CliAuth.Store.memory(), }) ``` Then plug `handler` into your server framework of choice: ```ts createServer(handler.listener) // Node.js Bun.serve({ fetch: handler.fetch }) // Bun Deno.serve({ fetch: handler.fetch }) // Deno app.all('*', c => handler.fetch(c.request)) // Elysia app.use(handler.listener) // Express app.use(c => handler.fetch(c.req.raw)) // Hono export const GET = handler.fetch // Next.js export const POST = handler.fetch // Next.js ``` ## Parameters ### chains * **Type:** `readonly [Chain, ...Chain[]]` (from `viem`) * **Default:** `[tempo, tempoModerato, tempoDevnet]` Supported chains. The handler resolves the client based on chain IDs carried by device-code requests and key authorizations. The first chain is used as the default when a request omits `chainId`. ```ts twoslash import { tempo } from 'viem/tempo/chains' import { Handler } from 'accounts/server' const handler = Handler.codeAuth({ chains: [tempo], // [!code focus] }) ``` ### maxBodyBytes * **Type:** `number` * **Default:** `16384` Maximum JSON request body size in bytes. Requests exceeding this limit are rejected with a `400` before parsing. ```ts twoslash import { Handler } from 'accounts/server' const handler = Handler.codeAuth({ maxBodyBytes: 8_192, // [!code focus] }) ``` ### now * **Type:** `() => number` * **Optional** Time source used for TTL evaluation. Defaults to `Date.now`. Useful for tests that need a deterministic clock. ```ts twoslash import { Handler } from 'accounts/server' const handler = Handler.codeAuth({ now: () => 1_700_000_000_000, // [!code focus] }) ``` ### path * **Type:** `string` * **Default:** `'/auth/pkce'` Path prefix for the code-auth endpoints. ```ts twoslash import { Handler } from 'accounts/server' const handler = Handler.codeAuth({ path: '/cli-auth', // [!code focus] }) ``` ### policy * **Type:** `CliAuth.Policy` * **Optional** Validates and optionally rewrites requested defaults (expiry, spending limits) before the pending entry is stored. Use this to enforce server-side policy on what the SDK is allowed to ask for. ```ts twoslash import { Handler } from 'accounts/server' const handler = Handler.codeAuth({ policy: { // [!code focus] validate: ({ expiry }) => ({ // [!code focus] expiry: Math.min(expiry ?? 0, Math.floor(Date.now() / 1000) + 3600), // [!code focus] }), // [!code focus] }, // [!code focus] }) ``` ### random * **Type:** `(size: number) => Uint8Array` * **Optional** Random byte generator used for device-code allocation. Defaults to the platform's `crypto.getRandomValues`. Override only for tests. ### rateLimit * **Type:** `CliAuth.RateLimit | false` * **Default:** `CliAuth.RateLimit.memory({ max: 120, windowMs: 60_000 })` Shared rate limiter applied to every code-auth endpoint. Pass `false` to disable rate limiting entirely (not recommended in production). ```ts twoslash // @noErrors import { Handler, CliAuth } from 'accounts/server' const handler = Handler.codeAuth({ rateLimit: CliAuth.RateLimit.cloudflare(env.RATE_LIMITER), // [!code focus] }) ``` ### rateLimitKey * **Type:** `(request: Request) => string` * **Default:** Cloudflare's `cf-connecting-ip` header, falling back to `'unknown'` Derives the rate-limit key from the incoming request. Override when fronting the handler with a different proxy that exposes the real client IP under another header. ```ts twoslash import { Handler } from 'accounts/server' const handler = Handler.codeAuth({ rateLimitKey: (req) => req.headers.get('x-real-ip') ?? 'unknown', // [!code focus] }) ``` ### store * **Type:** `CliAuth.Store` * **Default:** `CliAuth.Store.memory()` Device-code store. Persists pending, authorized, and consumed device-code entries. Use a durable backend in production so device codes survive process restarts. ```ts twoslash // @noErrors import { Handler, CliAuth } from 'accounts/server' const handler = Handler.codeAuth({ store: CliAuth.Store.kv(env.KV), // [!code focus] }) ``` ### transports * **Type:** `Record` (from `viem`) * **Default:** `http()` for each configured chain Transports keyed by chain ID. Override per chain to point at a private RPC endpoint. ```ts twoslash import { http } from 'viem' import { tempo } from 'viem/tempo/chains' import { Handler } from 'accounts/server' const handler = Handler.codeAuth({ transports: { // [!code focus] [tempo.id]: http('https://rpc.tempo.xyz'), // [!code focus] }, // [!code focus] }) ``` ### ttlMs * **Type:** `number` * **Default:** `600000` (10 minutes) Pending entry TTL in milliseconds. After this duration, pending device codes can no longer be authorized or polled. ```ts twoslash import { Handler } from 'accounts/server' const handler = Handler.codeAuth({ ttlMs: 5 * 60 * 1000, // [!code focus] }) ``` ### cors * **Type:** `boolean | Cors` * **Default:** `true` Inherited from [`from.Options`](/docs/server/handler.compose#optionscors). CORS configuration applied to every response. ### headers * **Type:** `Headers | Record` * **Optional** Inherited from [`from.Options`](/docs/server/handler.compose#optionsheaders). Additional headers merged onto every response. # `Handler.feePayer` (Deprecated) :::warning `Handler.feePayer` has been deprecated. Use [`Handler.relay`](/docs/server/handler.relay) with the [`feePayer`](/docs/server/handler.relay#feepayer) option instead. ::: ## Migration Replace `Handler.feePayer` calls with `Handler.relay` and nest the `account` under `feePayer`: ```diff - const handler = Handler.feePayer({ - account: privateKeyToAccount('0x...'), - }) + const handler = Handler.relay({ + feePayer: { + account: privateKeyToAccount('0x...'), + }, + }) ``` All other options (`chains`, `transports`, `path`, `onRequest`) remain the same — `validate` moves inside `feePayer`: ```diff - const handler = Handler.feePayer({ - account: privateKeyToAccount('0x...'), - validate: (request) => request.from !== blocked, - }) + const handler = Handler.relay({ + feePayer: { + account: privateKeyToAccount('0x...'), + validate: (request) => request.from !== blocked, + }, + }) ``` See [`Handler.relay`](/docs/server/handler.relay) for the full API reference. # `tempoWallet` Creates a Wagmi connector backed by the Tempo Wallet dialog adapter. ## Usage ```ts import { createConfig, http } from 'wagmi' import { tempo } from 'wagmi/chains' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ chains: [tempo], connectors: [tempoWallet()], transports: { [tempo.id]: http(), }, }) ``` ## Parameters Accepts all [`dialog`](/docs/api/dialog) adapter options, plus all [`Provider`](/docs/api/provider) options except `adapter`. ### accessKey.authorize * **Type:** `{ address?: Address; chainId?: bigint; expiry: number; keyType?: 'secp256k1' | 'p256' | 'webAuthn'; limits?: { token: Address; limit: bigint; period?: number }[]; privateKey?: Hex; publicKey?: Hex; scopes?: { address: Address; recipients?: Address[]; selector?: Hex | string }[] } | (() => { ... } | undefined)` * **Optional** Access-key parameters to authorize automatically when no stored key satisfies a request. Pass an object to use the same parameters for every request, or a function to compute them per request — return `undefined` to skip authorization for that request. This matches [`Provider`'s `accessKey.authorize`](/docs/api/provider#accesskeyauthorize). Tempo Wallet-hosted approval pages require at least one `limits` entry and one `scopes` entry. Local adapters accept unbounded keys, but production apps should pass both so local and wallet-hosted flows use the same policy. ```ts twoslash import { createConfig, http } from 'wagmi' import { tempo } from 'wagmi/chains' import { parseUnits } from 'viem' import { Expiry } from 'accounts' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ chains: [tempo], connectors: [ tempoWallet({ accessKey: { // [!code focus] authorize: { // [!code focus] expiry: Expiry.days(7), // [!code focus] limits: [{ // [!code focus] token: '0x20c0000000000000000000000000000000000001', // [!code focus] limit: parseUnits('500', 6), // [!code focus] }], // [!code focus] scopes: [{ // [!code focus] address: '0x20c0000000000000000000000000000000000001', // [!code focus] selector: 'transfer(address,uint256)', // [!code focus] }], // [!code focus] }, // [!code focus] }, // [!code focus] }), ], transports: { [tempo.id]: http() }, }) ``` ### dialog * **Type:** `Dialog` * **Default:** `Dialog.iframe()` (or `Dialog.popup()` in Safari/insecure contexts) Dialog to use for the embed app. ```ts twoslash import { createConfig, http } from 'wagmi' import { tempo } from 'wagmi/chains' import { Dialog } from 'accounts' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ chains: [tempo], connectors: [ tempoWallet({ dialog: Dialog.popup(), // [!code focus] }), ], transports: { [tempo.id]: http() }, }) ``` ### feePayer * **Type:** `string | { url: string; precedence?: 'fee-payer-first' | 'user-first' }` * **Optional** Fee payer configuration for interacting with a service running [`Handler.relay`](/docs/server/handler.relay) from `accounts/server`. Pass a URL string, or an object with `url` and optional `precedence`. :::info [See the guide](https://docs.tempo.xyz/guide/payments/sponsor-user-fees) ::: ```ts twoslash import { createConfig, http } from 'wagmi' import { tempo } from 'wagmi/chains' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ chains: [tempo], connectors: [ tempoWallet({ feePayer: 'https://myapp.com/fee-payer', // [!code focus] }), ], transports: { [tempo.id]: http() }, }) ``` ### host * **Type:** `string` * **Default:** `'https://wallet.tempo.xyz/embed'` URL of the embed app. ```ts twoslash import { createConfig, http } from 'wagmi' import { tempo } from 'wagmi/chains' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ chains: [tempo], connectors: [ tempoWallet({ host: 'https://wallet.tempo.xyz/embed', // [!code focus] }), ], transports: { [tempo.id]: http() }, }) ``` ### mpp * **Type:** `boolean` * **Default:** `false` Enable [Machine Payment Protocol](https://mpp.dev) support. ```ts twoslash import { createConfig, http } from 'wagmi' import { tempo } from 'wagmi/chains' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ chains: [tempo], connectors: [ tempoWallet({ mpp: true, // [!code focus] }), ], transports: { [tempo.id]: http() }, }) ``` ### testnet * **Type:** `boolean` * **Default:** `false` Use testnet. When `true`, the default chain will be the first testnet chain in `chains`. ```ts twoslash import { createConfig, http } from 'wagmi' import { tempo } from 'wagmi/chains' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ chains: [tempo], connectors: [ tempoWallet({ testnet: true, // [!code focus] }), ], transports: { [tempo.id]: http() }, }) ``` # `webAuthn` Creates a Wagmi connector backed by the WebAuthn adapter. ## Usage ```ts import { createConfig, http } from 'wagmi' import { tempo } from 'wagmi/chains' import { webAuthn } from 'wagmi/tempo' export const config = createConfig({ chains: [tempo], connectors: [webAuthn({ authUrl: '/auth' })], transports: { [tempo.id]: http(), }, }) ``` ## Parameters Accepts all [`webAuthn`](/docs/api/webAuthn) adapter options, plus all [`Provider`](/docs/api/provider) options except `adapter`. ### authUrl * **Type:** `string` * **Optional** URL of a [WebAuthn server handler](/docs/server/handler.webAuthn). :::info [See the guide](https://docs.tempo.xyz/guide/use-accounts/embed-passkeys) ::: :::warning Cannot be used with `ceremony`. ::: ```ts twoslash import { createConfig, http } from 'wagmi' import { tempo } from 'wagmi/chains' import { webAuthn } from 'wagmi/tempo' export const config = createConfig({ chains: [tempo], connectors: [ webAuthn({ authUrl: '/auth', // [!code focus] }), ], transports: { [tempo.id]: http() }, }) ``` ### accessKey.authorize * **Type:** `{ address?: Address; chainId?: bigint; expiry: number; keyType?: 'secp256k1' | 'p256' | 'webAuthn'; limits?: { token: Address; limit: bigint; period?: number }[]; privateKey?: Hex; publicKey?: Hex; scopes?: { address: Address; recipients?: Address[]; selector?: Hex | string }[] } | (() => { ... } | undefined)` * **Optional** Access-key parameters to authorize automatically when no stored key satisfies a request. Pass an object to use the same parameters for every request, or a function to compute them per request — return `undefined` to skip authorization for that request. This matches [`Provider`'s `accessKey.authorize`](/docs/api/provider#accesskeyauthorize). Tempo Wallet-hosted approval pages require at least one `limits` entry and one `scopes` entry. Local adapters accept unbounded keys, but production apps should pass both so local and wallet-hosted flows use the same policy. ```ts twoslash import { createConfig, http } from 'wagmi' import { tempo } from 'wagmi/chains' import { parseUnits } from 'viem' import { Expiry } from 'accounts' import { webAuthn } from 'wagmi/tempo' export const config = createConfig({ chains: [tempo], connectors: [ webAuthn({ authUrl: '/auth', accessKey: { // [!code focus] authorize: { // [!code focus] expiry: Expiry.days(7), // [!code focus] limits: [{ // [!code focus] token: '0x20c0000000000000000000000000000000000001', // [!code focus] limit: parseUnits('500', 6), // [!code focus] }], // [!code focus] scopes: [{ // [!code focus] address: '0x20c0000000000000000000000000000000000001', // [!code focus] selector: 'transfer(address,uint256)', // [!code focus] }], // [!code focus] }, // [!code focus] }, // [!code focus] }), ], transports: { [tempo.id]: http() }, }) ``` ### ceremony * **Type:** `WebAuthnCeremony` * **Default:** `WebAuthnCeremony.server({ url: authUrl }){:js}` Ceremony strategy for WebAuthn registration and authentication. [See more](/docs/api/webauthnceremony). :::info [See the guide](https://docs.tempo.xyz/guide/use-accounts/embed-passkeys) ::: :::warning Cannot be used with `authUrl`. ::: ```ts twoslash import { createConfig, http } from 'wagmi' import { tempo } from 'wagmi/chains' import { WebAuthnCeremony } from 'accounts' import { webAuthn } from 'wagmi/tempo' export const config = createConfig({ chains: [tempo], connectors: [ webAuthn({ ceremony: WebAuthnCeremony.server({ url: '/auth' }), // [!code focus] }), ], transports: { [tempo.id]: http() }, }) ``` ### feePayer * **Type:** `string | { url: string; precedence?: 'fee-payer-first' | 'user-first' }` * **Optional** Fee payer configuration for interacting with a service running [`Handler.relay`](/docs/server/handler.relay) from `accounts/server`. Pass a URL string, or an object with `url` and optional `precedence`. :::info [See the guide](https://docs.tempo.xyz/guide/payments/sponsor-user-fees) ::: ```ts twoslash import { createConfig, http } from 'wagmi' import { tempo } from 'wagmi/chains' import { webAuthn } from 'wagmi/tempo' export const config = createConfig({ chains: [tempo], connectors: [ webAuthn({ authUrl: '/auth', feePayer: 'https://myapp.com/fee-payer', // [!code focus] }), ], transports: { [tempo.id]: http() }, }) ``` ### mpp * **Type:** `boolean` * **Default:** `false` Enable [Machine Payment Protocol](https://mpp.dev) support. ```ts twoslash import { createConfig, http } from 'wagmi' import { tempo } from 'wagmi/chains' import { webAuthn } from 'wagmi/tempo' export const config = createConfig({ chains: [tempo], connectors: [ webAuthn({ authUrl: '/auth', mpp: true, // [!code focus] }), ], transports: { [tempo.id]: http() }, }) ``` ### testnet * **Type:** `boolean` * **Default:** `false` Use testnet. When `true`, the default chain will be the first testnet chain in `chains`. ```ts twoslash import { createConfig, http } from 'wagmi' import { tempo } from 'wagmi/chains' import { webAuthn } from 'wagmi/tempo' export const config = createConfig({ chains: [tempo], connectors: [ webAuthn({ authUrl: '/auth', testnet: true, // [!code focus] }), ], transports: { [tempo.id]: http() }, }) ``` # AWS KMS Use a custom adapter when account signing should happen through AWS-controlled infrastructure. ## Demo Target workspace: `examples/enterprise-aws-kms/`. :::warning\[Stub] This page needs a concrete KMS signing design before implementation: key type, address derivation, latency, authorization policy, and audit logging. ::: ## Integration Checklist * Define the KMS key lifecycle and signer address derivation. * Keep authorization policy server-side. * Map KMS failures into app-readable provider errors. * Test payment and revocation flows. ## Next Steps * [Bring Your Auth](/docs/enterprise/bring-your-auth) * [Custom adapter](/docs/adapters/custom) * [Fee Sponsorship](/docs/guides/fee-sponsorship) # Custom Auth Use a custom adapter when your own infrastructure owns identity, authorization, and signing. ## Demo Target workspace: `examples/enterprise-custom-auth/`. :::warning\[Stub] This page needs customer-pattern input before implementation. The final guide should pick one concrete architecture instead of describing every possible signer shape. ::: ## Integration Checklist * Define the trust boundary between browser, server, and signer. * Decide which service proves user intent. * Keep signer credentials out of app code. * Run the shared guide suite through the adapter. ## Next Steps * [Custom adapter](/docs/adapters/custom) * [Hosted Universal Wallets](/docs/enterprise/hosted-universal-wallets) * [Authentication](/docs/guides/authentication) # Privy Use the built-in [Privy adapter](/docs/adapters/privy) when Privy owns authentication, session state, and embedded wallets while the app still wants Tempo account features through the Accounts SDK. Use `privy` and `PrivyAccountsBridge` from `accounts/react/privy`. ## Demo Target workspace: `examples/enterprise-privy/`. :::tip Start with the [Privy adapter guide](/docs/adapters/privy) and the [`privy` API reference](/docs/api/privy). This page is reserved for enterprise deployment notes that go beyond the adapter setup. ::: ## Integration Checklist * Confirm which Privy login methods your app supports. * Ensure new users receive embedded Ethereum wallets during signup. * Use the `accounts/react/privy` helper unless you need a custom account-selection bridge. * Decide whether account selection should expose every embedded wallet or a selected subset. * Run the connect and payment guides through the Privy adapter. ## Next Steps * [Bring Your Auth](/docs/enterprise/bring-your-auth) * [Privy adapter](/docs/adapters/privy) * [`privy` API reference](/docs/api/privy) # Turnkey Use a custom adapter when Turnkey owns signer infrastructure and the app wants to expose Tempo account operations through the Tempo Accounts SDK. ## Demo Target workspace: `examples/enterprise-turnkey/`. :::warning\[Stub] This page needs a real Turnkey-backed adapter example before publishing. Keep the root SDK structurally typed so non-Turnkey consumers do not inherit a hard dependency. ::: ## Integration Checklist * Accept an app-provided Turnkey client shape. * Keep Turnkey-specific dependencies out of the root SDK path. * Map Turnkey signing responses into Tempo Accounts SDK responses. * Test connect, payment, and spend-permission flows. ## Next Steps * [Bring Your Auth](/docs/enterprise/bring-your-auth) * [Custom adapter](/docs/adapters/custom) * [Spend Permissions](/docs/guides/spend-permissions)