# 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 ++]
)
}
```
### 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 (
)
}
```
### 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 (
)
}
```
## 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 (
)
}
```
::::
## 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 (
)
}
```
::::
## 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 `