<!--
Sitemap:
- [Tempo Accounts SDK - Getting Started](/docs/): Set up the Tempo Accounts SDK to create, manage, and interact with accounts on Tempo.
- [Deploying to Production](/docs/production): Things to consider before deploying your application with the Tempo Accounts SDK to production.
- [FAQ](/docs/faq): Frequently asked questions about the Tempo Accounts SDK.
- [Connect Accounts](/docs/guides/connect-accounts): Connect a Tempo account in your application.
- [Authentication](/docs/guides/authentication): Authenticate connected accounts against your own server with a signed SIWE challenge.
- [Identity](/docs/guides/identity): Request and verify identity claims about a connected account, starting with a verified email.
- [Transfers](/docs/guides/transfers): Send stablecoin transfers from a connected Tempo account, initiated either by the user or by your server.
- [Spend Permissions](/docs/guides/spend-permissions): Authorize spend limits, call scopes, and expiries so repeat transfers can be signed without a confirmation prompt.
- [React Native](/docs/guides/react-native): Set up Tempo Accounts in a React Native app.
- [Subscriptions](/docs/guides/subscriptions): Charge recurring payments from a connected Tempo account.
- [Fee Sponsorship](/docs/guides/fee-sponsorship): Sponsor transaction fees from a server-controlled policy.
- [Deposits](/docs/guides/deposits): Open the Tempo deposit flow from a connected account and let the user choose deposit details.
- [Swaps](/docs/guides/swaps): Open the Tempo swap flow from a connected account with optional pre-filled intent fields.
- [Theming](/docs/guides/theming): Match embedded account surfaces to your product.
- [CLI](/docs/guides/cli): Authorize and use Tempo accounts from command-line tools.
- [Adapters](/docs/adapters/): Choose the signing adapter for your Tempo Accounts SDK integration.
- [Tempo Wallet Adapter](/docs/adapters/tempo-wallet): Use Tempo Wallet as the hosted universal wallet adapter.
- [WebAuthn Adapter](/docs/adapters/webauthn): Use domain-bound passkeys as the account signing adapter.
- [Turnkey Adapter](/docs/adapters/turnkey): Use Turnkey-managed wallet accounts as the account signing adapter.
- [Privy Adapter](/docs/adapters/privy): Use Privy embedded wallets as the account signing adapter.
- [Private Key Adapter](/docs/adapters/private-key): Sign in-process with a `secp256k1` private key.
- [Custom Adapter](/docs/adapters/custom): Author your own adapter with the `Adapter.define` API.
- [Adapters](/docs/api/adapters): Pluggable adapters for the Tempo Accounts SDK Provider.
- [dialog](/docs/api/dialog): Adapter for the Tempo Wallet dialog, an embedded iframe or popup for account management.
- [local](/docs/api/local): Key-agnostic adapter for defining arbitrary account types and signing mechanisms.
- [mobileWebAuth](/docs/api/mobileWebAuth): Mobile web auth adapter for browser-session wallet requests.
- [postMessage](/docs/api/postMessage): Connect to wallet from anywhere on the web
- [privy](/docs/api/privy): React adapter backed by Privy sessions and embedded Ethereum wallets.
- [secp256k1](/docs/api/secp256k1): Adapter that signs in-process with a `secp256k1` private key.
- [turnkey](/docs/api/turnkey): Adapter backed by Turnkey client sessions.
- [webAuthn](/docs/api/webAuthn): Adapter for passkey-based accounts using WebAuthn registration and authentication.
- [Dialog](/docs/api/dialogs): Dialog modes for embedding the Tempo Wallet.
- [Dialog.iframe](/docs/api/dialog.iframe): Embed the Tempo Wallet auth UI in an iframe dialog element.
- [Dialog.popup](/docs/api/dialog.popup): Open the Tempo Wallet auth UI in a popup window.
- [Expiry](/docs/api/expiry): Utility functions for computing access key expiry timestamps.
- [Provider](/docs/api/provider): Create an EIP-1193 provider for managing accounts on Tempo.
- [Rpc](/docs/api/rpc): Per-method Zod schemas and shared building blocks for the Accounts JSON-RPC surface.
- [Schema](/docs/api/schema): Zod-based JSON-RPC schema definitions for the Accounts provider.
- [TrustedHosts](/docs/api/trustedHosts): Trusted host mappings and matching helpers for dialog adapters.
- [WebAuthnCeremony](/docs/api/webauthnceremony): Pluggable strategy for WebAuthn registration and authentication ceremonies.
- [WebAuthnCeremony.from](/docs/api/webauthnceremony.from): Create a WebAuthnCeremony from a custom implementation.
- [WebAuthnCeremony.server](/docs/api/webauthnceremony.server): Server-backed WebAuthn ceremony that delegates to a remote handler.
- [CLI](/docs/cli/provider): Create a Provider for CLI environments.
- [eth_accounts](/docs/rpc/eth_accounts): Get the addresses of the currently connected accounts.
- [eth_chainId](/docs/rpc/eth_chainId): Get the chain ID of the currently active chain.
- [eth_fillTransaction](/docs/rpc/eth_fillTransaction): Fills missing transaction fields and returns wallet-aware metadata.
- [eth_requestAccounts](/docs/rpc/eth_requestAccounts): Request access to user accounts, prompting the user to connect if needed.
- [eth_sendTransaction](/docs/rpc/eth_sendTransaction): Send a transaction from the connected account.
- [eth_sendTransactionSync](/docs/rpc/eth_sendTransactionSync): Send a transaction and wait for the receipt.
- [eth_signTransaction](/docs/rpc/eth_signTransaction): Sign a transaction without broadcasting it.
- [eth_signTypedData_v4](/docs/rpc/eth_signTypedData_v4): Sign EIP-712 typed structured data with the connected account.
- [personal_sign](/docs/rpc/personal_sign): Sign a message with the connected account.
- [wallet_authorizeAccessKey](/docs/rpc/wallet_authorizeAccessKey): Authorize an access key for delegated transaction signing.
- [wallet_connect](/docs/rpc/wallet_connect): Connect account(s) with optional capabilities like access key authorization.
- [wallet_deposit](/docs/rpc/wallet_deposit): Open the wallet deposit flow with optional pre-filled fields.
- [wallet_depositZone](/docs/rpc/wallet_depositZone): Open the wallet zone-deposit flow with optional pre-filled fields.
- [wallet_disconnect](/docs/rpc/wallet_disconnect): Disconnect the connected account(s).
- [wallet_getBalances](/docs/rpc/wallet_getBalances): Get token balances for an account.
- [wallet_getCallsStatus](/docs/rpc/wallet_getCallsStatus): Get the status of a batch of calls sent via wallet_sendCalls.
- [wallet_getCapabilities](/docs/rpc/wallet_getCapabilities): Get account capabilities for specified chains.
- [wallet_revokeAccessKey](/docs/rpc/wallet_revokeAccessKey): Revoke a previously authorized access key.
- [wallet_send](/docs/rpc/wallet_send): Open the wallet send-token flow with optional pre-filled fields.
- [wallet_sendCalls](/docs/rpc/wallet_sendCalls): Send a batch of calls from the connected account.
- [wallet_swap](/docs/rpc/wallet_swap): Open the wallet swap flow with optional pre-filled swap intent fields.
- [wallet_switchEthereumChain](/docs/rpc/wallet_switchEthereumChain): Switch the provider's active chain.
- [wallet_withdrawZone](/docs/rpc/wallet_withdrawZone): Open the wallet zone-withdraw flow with optional pre-filled fields.
- [Remote](/docs/api/remote): Bridge that runs inside the wallet's iframe/popup and serves RPC requests from the host SDK.
- [Remote.create](/docs/api/remote.create): Create a remote context bound to a Messenger and Provider.
- [Remote.useEnsureVisibility](/docs/api/remote.useEnsureVisibility): React hook that monitors iframe visibility and falls back to a popup when occluded.
- [Remote.useState](/docs/api/remote.useState): React hook to subscribe to a remote context's state store.
- [Remote.useTheme](/docs/api/remote.useTheme): React hook that applies theme overrides from URL search params and live messenger updates.
- [Remote.validateSearch](/docs/api/remote.validateSearch): Validate an RPC request payload from URL search params.
- [Tempo Accounts Server Handlers](/docs/server/): Configure server-side Tempo Accounts SDK handlers for relaying wallet RPC requests, composing backends, and managing WebAuthn ceremonies.
- [Handler.auth](/docs/server/handler.auth): Server handler that issues SIWE-based authentication challenges and sessions.
- [Handler.codeAuth](/docs/server/handler.codeAuth): Server handler for the device-code (PKCE) access-key bootstrap flow.
- [Handler.compose](/docs/server/handler.compose): Compose multiple server handlers into a single handler.
- [Handler.exchange](/docs/server/handler.exchange): Server handler that returns Stablecoin DEX quotes and ready-to-submit calls.
- [Handler.relay](/docs/server/handler.relay): Server handler that proxies certain RPC requests with wallet-aware enrichment.
- [Handler.webAuthn](/docs/server/handler.webAuthn): Server-side WebAuthn ceremony handler for registration and authentication.
- [hc](/docs/server/hc): Typed RPC client for handlers built with the Tempo Accounts SDK.
- [Identity.verify](/docs/server/identity.verify): Verify a wallet-issued identity token (verified email) against an issuer's JWKS.
- [Kv](/docs/server/kv): Key-value store adapters for server-side persistence.
- [Kv.cloudflare](/docs/server/kv.cloudflare): Kv adapter backed by a Cloudflare Workers KV namespace.
- [Kv.durableObject](/docs/server/kv.durableObject): Kv adapter backed by a Cloudflare Durable Object with atomic take and create.
- [Kv.from](/docs/server/kv.from): Wrap a custom Kv-shaped object so the SDK accepts it as a Kv.
- [Kv.memory](/docs/server/kv.memory): In-memory Kv adapter for tests and single-process deployments.
- [Keystore](/docs/api/keystore): Pluggable backends for key material.
- [Keystore.p256](/docs/api/keystore.p256): Pure-JS P-256 keystore.
- [Keystore.secp256k1](/docs/api/keystore.secp256k1): Pure-JS secp256k1 keystore.
- [Keystore.webCryptoP256](/docs/api/keystore.webCryptoP256): WebCrypto P-256 keystore.
- [Storage](/docs/api/storage): Pluggable storage adapters for persisting provider state.
- [Storage.combine](/docs/api/storage.combine): Combine multiple Storage adapters into one.
- [Storage.cookie](/docs/api/storage.cookie): Cookie-backed Storage adapter.
- [Storage.from](/docs/api/storage.from): Create a Storage adapter from a custom implementation.
- [Storage.idb](/docs/api/storage.idb): IndexedDB-backed Storage adapter.
- [Storage.localStorage](/docs/api/storage.localStorage): localStorage-backed Storage adapter.
- [Storage.memory](/docs/api/storage.memory): In-memory Storage adapter.
- [asyncStorage](/docs/api/storage.asyncStorage): React Native Storage adapter backed by AsyncStorage.
- [secureMmkv](/docs/api/storage.secureMmkv): Encrypted React Native Storage adapter backed by MMKV.
- [Tempo Accounts SDK](/index): The fastest way to add stablecoins to your application.
- [Secp256k1 Adapter](/docs/adapters/secp256k1): Sign in-process with a `secp256k1` private key.
- [Bring Your Auth](/docs/enterprise/bring-your-auth/): Connect enterprise auth and signing systems to Tempo accounts.
- [Hosted Universal Wallets](/docs/enterprise/hosted-universal-wallets): Stub for hosting a universal wallet on your own domain.
- [Handler.feePayer (Deprecated)](/docs/server/handler.feePayer): Deprecated — use Handler.relay with feePayer option instead.
- [tempoWallet](/docs/wagmi/tempoWallet): Wagmi connector for the Tempo Wallet dialog.
- [webAuthn](/docs/wagmi/webAuthn): Wagmi connector for passkey-based WebAuthn accounts.
- [AWS KMS](/docs/enterprise/bring-your-auth/aws-kms): Stub for integrating AWS KMS-backed signing with the Tempo Accounts SDK.
- [Custom Auth](/docs/enterprise/bring-your-auth/custom): Stub for first-party enterprise auth and signing integrations.
- [Privy](/docs/enterprise/bring-your-auth/privy): Enterprise notes for integrating Privy-backed auth with the Tempo Accounts SDK.
- [Turnkey](/docs/enterprise/bring-your-auth/turnkey): Stub for integrating Turnkey-backed signing with the Tempo Accounts SDK.
-->

# 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.

<Demo title="Subscription Payment" githubUrl="https://github.com/tempoxyz/accounts/tree/main/examples/subscriptions" headerAction={<DemoReset />} className="flex flex-col gap-3">
  <Steps.Step
    label={
    <SubscriptionStepLabel badge="User">
      Create an account, or use an existing one.
    </SubscriptionStepLabel>
  }
    action={<ConnectAccount />}
  />

  <SubscriptionPayment />
</Demo>

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,
    }),
  ],
})
```

:::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 (
    <button disabled={articles.isPending} onClick={() => articles.mutate()}>
      {articles.isPending ? 'Loading...' : 'GET /api/articles'}
    </button>
  )
}
```

### 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,
    }),
  ],
})
```

### 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

<CheckboxCards>
  <CheckboxCard id="subscriptions.durable-store" href="#persist-subscription-state">
    A durable `store` backs `mppx` subscription records.
  </CheckboxCard>

  <CheckboxCard id="subscriptions.app-database" href="#persist-subscription-state">
    Your app database stores `subscriptionId`, lookup key, plan, next due time, and status.
  </CheckboxCard>

  <CheckboxCard id="subscriptions.scheduled-collection" href="#collect-from-a-schedule">
    Collection runs from cron, a queue, or a worker scheduler.
  </CheckboxCard>

  <CheckboxCard id="subscriptions.idempotent-renew" href="#treat-renew-as-idempotent">
    `renew` returning `null` is handled as nothing due.
  </CheckboxCard>

  <CheckboxCard id="subscriptions.fee-payer-monitoring" href="#monitor-collection-failures">
    The fee payer is configured and its balance is monitored.
  </CheckboxCard>

  <CheckboxCard id="subscriptions.customer-lifecycle" href="#keep-periods-reviewable">
    Cancellation, expiry, and customer notification flows are in place.
  </CheckboxCard>

  <CheckboxCard id="subscriptions.production-periods" href="#keep-periods-reviewable">
    Local-only periods such as `dev_second` stay out of production.
  </CheckboxCard>
</CheckboxCards>

## Next Steps

<Cards>
  <Card description="See the complete subscriptions example with a protected route and manual collection helper." icon="lucide:repeat" to="https://github.com/tempoxyz/accounts/tree/main/examples/subscriptions" title="Subscriptions Example" />

  <Card description="Authorize bounded access keys for repeated account activity." icon="lucide:shield" to="/docs/guides/spend-permissions" title="Spend Permissions" />

  <Card description="Charge a one-time server-initiated payment before returning a response." icon="lucide:send" to="/docs/guides/transfers" title="Transfers" />

  <Card description="Review HTTPS, trusted host, and adapter-specific production requirements." icon="lucide:rocket" to="/docs/production" title="Production" />
</Cards>
