> **Can't find what you're looking for?** Use `search_docs` on the docs MCP server at `https://accounts.tempo.xyz/api/mcp` to find what you need.

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

#### Subscription Payment

[Runnable example source](https://github.com/tempoxyz/accounts/tree/main/examples/subscriptions)

Create an account, or use an existing one.

Interactive demo: Make a subscription payment. Run it in the browser or use the linked example source.

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

[Requirement details](/docs/guides/subscriptions#persist-subscription-state)

Your app database stores `subscriptionId`, lookup key, plan, next due time, and status.

[Requirement details](/docs/guides/subscriptions#persist-subscription-state)

Collection runs from cron, a queue, or a worker scheduler.

[Requirement details](/docs/guides/subscriptions#collect-from-a-schedule)

`renew` returning `null` is handled as nothing due.

[Requirement details](/docs/guides/subscriptions#treat-renew-as-idempotent)

The fee payer is configured and its balance is monitored.

[Requirement details](/docs/guides/subscriptions#monitor-collection-failures)

Cancellation, expiry, and customer notification flows are in place.

[Requirement details](/docs/guides/subscriptions#keep-periods-reviewable)

Local-only periods such as `dev_second` stay out of production.

[Requirement details](/docs/guides/subscriptions#keep-periods-reviewable)

## Next Steps

[Subscriptions Example](https://github.com/tempoxyz/accounts/tree/main/examples/subscriptions): See the complete subscriptions example with a protected route and manual collection helper.

[Spend Permissions](/docs/guides/spend-permissions): Authorize bounded access keys for repeated account activity.

[Transfers](/docs/guides/transfers): Charge a one-time server-initiated payment before returning a response.

[Production](/docs/production): Review HTTPS, trusted host, and adapter-specific production requirements.
