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

import { MermaidDiagram } from '../../components/MermaidDiagram'

# Create and manage subscriptions \[Recurring access for paid APIs]

## Choose a signing account

### Direct

Create a server-only wallet.ts module, then import account wherever an example creates a local signing account.

```ts
import { privateKeyToAccount } from 'viem/accounts'

export const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`)
```

### Privy

Create an EVM wallet in Privy, fund it with the required currency on this page's network, and keep PRIVY\_APP\_SECRET server-side.

```bash
pnpm add @privy-io/node
```

Create a server-only privy.ts module, then import its account wherever an example configures account or feePayer.

```ts
import { PrivyClient } from '@privy-io/node'
import { createViemAccount } from '@privy-io/node/viem'

const privy = new PrivyClient({
  appId: process.env.PRIVY_APP_ID!,
  appSecret: process.env.PRIVY_APP_SECRET!,
})

export const account = createViemAccount(privy, {
  address: process.env.PRIVY_WALLET_ADDRESS as `0x${string}`,
  walletId: process.env.PRIVY_WALLET_ID!,
})
```

createViemAccount delegates signatures to the Privy wallet, so it replaces any local viem account in the examples on this page.

Build a subscription-gated API that charges $1 for access using `mppx` and Tempo subscriptions.

## Overview

Subscriptions separate access from billing. The client authorizes recurring access once, then keeps using the paid API without paying again on every request.

Your server stores the subscription after the first payment succeeds. Later requests prove the same payer signed the request, check the stored subscription, and return the protected response when access is active. Billing happens on its own schedule: the next request after a billing period ends can renew the subscription, or a background job can renew subscriptions before clients return. This keeps normal API requests simple while recurring payments happen asynchronously.

```mermaid
sequenceDiagram
  participant Client
  participant Server
  participant Store
  participant Tempo
  Client->>Server: GET /api/pro
  Server->>Store: Resolve subscription key
  Server-->>Client: 402 Challenge
  Client->>Server: Retry with keyAuthorization Credential
  Server->>Tempo: Charge first billing period
  Server->>Store: Store subscription record
  Server-->>Client: 200 OK + Receipt
  Client->>Server: GET /api/pro
  Server->>Store: Find active subscription
  Server-->>Client: 200 OK + Receipt

```

## Install `mppx`

:::code-group
```bash [npm]
$ npm install mppx viem
```

```bash [pnpm]
$ pnpm add mppx viem
```

```bash [bun]
$ bun add mppx viem
```
:::

## Create the subscription method

Create one `Mppx` instance and register `tempo.subscription()`.

```ts twoslash [mppx.server.ts]
import { Mppx, Store, tempo } from 'mppx/server'

const store = Store.memory()

export const mppx = Mppx.create({
  methods: [
    tempo.subscription({
      amount: '1.00',
      currency: '0x20c0000000000000000000000000000000000000',
      periodCount: '1',
      periodUnit: 'week',
      recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
      requireCredential: true,
      resolve: async ({ source }) => {
        if (!source) return null
        return { key: `payer:${source.chainId}:${source.address}:plan:pro` }
      },
      store,
      subscriptionExpires: new Date('2027-01-01T00:00:00.000Z'),
      testnet: true,
    }),
  ],
})
```

The `resolve` function maps each verified payer to one subscription. `requireCredential` makes the server derive access from the signed payer source instead of request headers.

:::warning
Use a durable atomic store in production. `Store.memory()` loses subscription state when the process restarts.
:::

## Gate the API route

Call `mppx.tempo.subscription({})` before returning paid data.

```ts twoslash [route.ts]
import { Mppx, Store, tempo } from 'mppx/server'

const store = Store.memory()

const mppx = Mppx.create({
  methods: [
    tempo.subscription({
      amount: '1.00',
      currency: '0x20c0000000000000000000000000000000000000',
      periodCount: '1',
      periodUnit: 'week',
      recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
      requireCredential: true,
      resolve: async ({ source }) => {
        if (!source) return null
        return { key: `payer:${source.chainId}:${source.address}:plan:pro` }
      },
      store,
      subscriptionExpires: new Date('2027-01-01T00:00:00.000Z'),
      testnet: true,
    }),
  ],
})
// ---cut---

export async function GET(request: Request) {
  const result = await mppx.tempo.subscription({})(request)

  if (result.status === 402) return result.challenge

  return result.withReceipt(
    Response.json({
      limits: { requests: 100_000 },
      plan: 'pro',
    }),
  )
}
```

The first unpaid request returns `402`. After the client activates the subscription, the same route returns `200` with a `Payment-Receipt` header.

## Configure the client

Register `tempo.subscription()` on the client. The SDK handles the `402` response, signs the key authorization, and retries the request.

### Accounts SDK

```ts twoslash [client.ts]
import { Mppx, tempo } from 'mppx/client'
import { Provider } from 'accounts'

const provider = Provider.create({ mpp: false }) // Avoid double 402 handling; mppx is configured below.
await provider.request({ method: 'wallet_connect' })

Mppx.create({
  methods: [tempo.subscription({
      account: provider.getAccount({ signable: true }),
      getClient: provider.getClient,
    })],
})

const response = await fetch('https://api.example.com/api/pro')

console.log(response.status)
// @log: 200
```

### viem

```ts twoslash [client.ts]
import { Mppx, tempo } from 'mppx/client'
import { privateKeyToAccount } from 'viem/accounts'

const account = privateKeyToAccount('0xabc…123')

Mppx.create({
  methods: [tempo.subscription({ account })],
})

const response = await fetch('https://api.example.com/api/pro')

console.log(response.status)
// @log: 200
```

## Advanced options

### Cancel a subscription

Cancellation is a server-side state change. Have the client call your cancellation endpoint, then mark the active subscription record with `canceledAt`; the next paid request returns `402` and requires a new subscription activation.

```ts [client.ts]
await fetch('/api/subscription/cancel', {
  method: 'POST',
})
```

```ts twoslash [cancel.ts]
import { Store } from 'mppx/server'
import { Subscription } from 'mppx/tempo'

const store = Store.memory()
const subscriptions = Subscription.fromStore(store)

export async function cancelSubscription(userId: string) {
  const subscription = await subscriptions.getByKey(`user:${userId}:plan:pro`)
  if (!subscription) return false

  await subscriptions.put({
    ...subscription,
    canceledAt: new Date().toISOString(),
  })

  return true
}
```

Keep the canceled record instead of deleting it. That preserves Receipts and prevents in-flight renewals from clearing the cancellation marker.

### Revoke the access key

On Tempo, clients can also revoke the authorized access key. Do this after server cancellation when you want a wallet-level backstop against future renewals.

```ts twoslash [revoke.ts]
import { createClient, http } from 'viem'
import { tempo } from 'viem/chains'
import { privateKeyToAccount } from 'viem/accounts'
import { Actions } from 'viem/tempo'

const client = createClient({
  account: privateKeyToAccount(
    '0x0000000000000000000000000000000000000000000000000000000000000001', // your account
  ),
  chain: tempo,
  transport: http(),
})

await Actions.accessKey.revokeSync(client, {
  accessKey: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8',
})
```

Revoking the access key doesn't cancel the merchant-side subscription record. If the client only revokes the key, the server can still reuse an already-paid period until it tries to renew.

### Renew in the background

Subscriptions renew when a request arrives after the next billing period starts. For proactive billing, run `tempo.renewSubscription()` from a background job.

```ts twoslash [renew.ts]
import type { Store } from 'mppx/server'
import { tempo } from 'mppx/server'

declare const store: Store.AtomicStore<Record<string, unknown>>

const result = await tempo.renewSubscription({
  store,
  subscriptionId: 'sub_abc123',
})

if (result) console.log(result.receipt.status)
```

## Production checklist

* Store subscription records in a durable atomic store.
* Mark canceled subscriptions with `canceledAt` and keep their records for audit.
* Use authenticated user or organization IDs in `resolve`.
* Set `subscriptionExpires` to the maximum authorization lifetime you accept.
* Add webhook or cron coverage for background renewals if access must stay warm.
* Persist `subscriptionId` and `externalId` in your app database for support and reconciliation.

## Next steps

* Read the [Tempo subscription overview](/payment-methods/tempo/subscription).
* Review [`tempo.subscription` server API](/sdk/typescript/server/Method.tempo.subscription).
* Review [`tempo.subscription` client API](/sdk/typescript/client/Method.tempo.subscription).
