<!-- tempo-docs-context -->
> Source: [/docs/server/relay-handler](https://tempo.xyz/developers/docs/server/relay-handler.md)
> Section: [Get Started](https://tempo.xyz/developers/get-started.md). Choose a starting point, send a test payment, get testnet funds, connect a coding agent, and find the SDKs and CLI.
> [Documentation index](https://tempo.xyz/developers/llms.txt) · [Docs skill](https://tempo.xyz/developers/SKILL.md)

> Tempo MCP: Use `search`, `find_pages`, `read_page`, and `code` at `https://mcp.tempo.xyz` for Tempo and related documentation.
> If an indexed page is missing or reflects an older site, read this site’s [documentation index](https://tempo.xyz/developers/llms.txt) and Markdown pages directly.

# Relay & Fee Payer Handler

Use `Handler.relay` to proxy RPC requests through your backend. The handler can enrich `eth_fillTransaction` with conditional fee sponsorship, fee-token selection, balance simulation, fee estimates, and automatic Stablecoin DEX swaps.

## Create a relay handler

Apply the [Tempo API dependency override](https://tempo.xyz/developers/docs/sdk/typescript/api-client#install-tempo-api-package), then install `tapimo@0.18.2` with `viem@2.56.9` in the relay server. This release of the handler depends on Viem exports removed in `2.57`; application clients can use the current Viem release independently.

```bash
pnpm add --save-exact tapimo@0.18.2 viem@2.56.9
```

Create the handler with an optional [Tempo API key](https://tempo.xyz/developers/docs/api/authentication), then expose either its Fetch API or Node.js listener entrypoint:

```ts
import { Handler } from 'tapimo/server'

const handler = Handler.relay({
  apiKey: process.env.TEMPO_API_KEY,
})
```

By default, the relay configures Viem clients for Tempo mainnet and testnet and loads fee-token candidates from the [Tempo API verified-token list](https://tempo.xyz/developers/docs/api/verified-tokens#gettokenlist). Pass `apiKey` to authenticate the built-in RPC and verified-token requests. When you omit it, the clients use each chain's default Viem transport and the verified-token request is unauthenticated.

:::info
The built-in client defaults to mainnet when a request does not specify a chain. Use testnet chain ID `42431` with Sandbox API keys.
:::

```ts twoslash
// @noErrors
import { createServer } from 'node:http'
import { Handler } from 'tapimo/server'

const handler = Handler.relay({ apiKey: process.env.TEMPO_API_KEY })
// ---cut---
createServer(handler.listener)              // Node.js
Bun.serve({ fetch: handler.fetch })         // Bun
Deno.serve(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
```

## Relay handler features

* [Sponsorship](#sponsorship) — Approve transactions and sign their fee payer payload with a server-controlled account.
* [Automatic swaps](#auto-swap) — Insert Stablecoin DEX calls when an account lacks a required token.
* [Fee-token selection](#best-fee-tokens) — Resolve a suitable fee token from account preferences and balances.
* [Balance diffs](#balance-diffs) — Return simulated token balance changes for the transaction sender.
* [Fee estimates](#fee-derivation) — Return fee estimates in raw token units and human-readable form.
* [Funding requirements](#require-funds) — Describe the exact token deficit when an account needs more funds.

<span id="sponsorship" />

### Fee sponsorship

Set [`feePayer`](#feepayer) to have the relay sign the fee payer payload for approved transactions. Add [`feePayer.validate`](#feepayervalidate) when your sponsorship policy should accept only specific senders or transactions. If validation rejects a request, the relay fills it again for self-payment.

The configured fee payer also handles approved `eth_signRawTransaction`, `eth_sendRawTransaction`, and `eth_sendRawTransactionSync` requests.

:::code-group
```ts twoslash [server.ts]
// @noErrors
import { createServer } from 'node:http'
import { Handler } from 'tapimo/server'
import { isAddressEqual } from 'viem'
import { Account } from 'viem/tempo'

const blockedAddress = '0x000000000000000000000000000000000000dead'
// ---cut---
const handler = Handler.relay({
  apiKey: process.env.TEMPO_API_KEY,
  feePayer: {
    account: Account.fromSecp256k1('0x...'),
    name: 'My App',
    url: 'https://myapp.com',
    // Optional sponsorship policy.
    validate: (request) =>
      request.from !== undefined && !isAddressEqual(request.from, blockedAddress),
  },
})

createServer(handler.listener).listen(3000)
```

```ts twoslash [client.ts]
// @noErrors
import { Provider } from 'accounts'

const provider = Provider.create({ relay: 'http://localhost:3000' })
const [account] = await provider.request({ method: 'eth_requestAccounts' })
// ---cut---
const result = await provider.request({
  method: 'eth_fillTransaction',
  params: [{
    from: account,
    calls: [{
      to: '0x20c000000000000000000000b9537d11c60e8b50',
      data: '0xa9059cbb000000000000000000000000cafebabecafebabecafebabecafebabecafebabe0000000000000000000000000000000000000000000000000000000005f5e100',
    }],
  }],
})

if (!('capabilities' in result))
  throw new Error('The relay did not return Tempo capabilities')

result.capabilities.sponsored
// true

result.capabilities.sponsor
// {
//   address: '0x1234567890abcdef1234567890abcdef12345678',
//   name: 'My App',
//   url: 'https://myapp.com',
// }
```
:::

:::warning
Without `feePayer.validate`, the handler sponsors every eligible request it receives. Add authentication, a sponsorship policy, rate limits, and logging before deploying a funded fee payer.
:::

<span id="auto-swap" />

### Automatic swaps

When an account lacks a token required by the transaction, the relay can prepend `approve` and `buy` calls through the [Stablecoin DEX](https://tempo.xyz/developers/docs/guide/stablecoin-dex). The response describes the injected calls and swap limits in `capabilities.autoSwap`. Swap-related changes are excluded from `capabilities.balanceDiffs`.

Enable this behavior with [`autoSwap`](#autoswap) or [`features: 'all'`](#features):

:::code-group
```ts [server.ts]
import { Handler } from 'tapimo/server'

const handler = Handler.relay({
  apiKey: process.env.TEMPO_API_KEY,
  autoSwap: { slippage: 0.05 },
})
```

```ts [response.ts]
result.capabilities.autoSwap
// {
//   calls: [
//     { to: '0x20c0...', data: '0x095ea7b3...' },
//     { to: '0x20c0...', data: '0x...' },
//   ],
//   maxIn: {
//     decimals: 6,
//     formatted: '105.000000',
//     name: 'OpenUSD',
//     symbol: 'OUSD',
//     token: '0x20c0000000000000000000006a37da5c996874be',
//     value: '0x6422c40',
//   },
//   minOut: {
//     decimals: 6,
//     formatted: '100.000000',
//     name: 'USDC.e',
//     symbol: 'USDC.e',
//     token: '0x20c000000000000000000000b9537d11c60e8b50',
//     value: '0x5f5e100',
//   },
//   slippage: 0.05,
// }
```
:::

<span id="best-fee-tokens" />

### Fee-token selection

For sponsored requests, `feePayer.feeToken` takes precedence over the request's explicit `feeToken`. If neither is set, the relay and upstream fill resolve a token from the available candidates. The relay does not inspect the sender's balances because the sponsor pays the fee.

For self-paid requests with fee-token resolution enabled, the relay selects a token in this order:

1. Use the request's explicit `feeToken`.
2. Use the account's onchain preference when the account has a balance. When [`cache`](#cache) is configured, the relay caches this lookup for about 60 seconds.
3. Choose the highest-balance token returned by [`resolveTokens`](#resolvetokens). The relay also considers TIP-20 tokens called by the transaction.
4. Leave `feeToken` unset for the upstream fill when no candidate has a positive balance.

Set `features: 'all'` to enable fee-token resolution with the Tempo API verified-token list for mainnet and testnet. Passing `resolveTokens` also enables fee-token resolution and replaces these candidates. Self-paid fills also consider TIP-20 call targets.

```ts
import { Handler } from 'tapimo/server'

const handler = Handler.relay({
  apiKey: process.env.TEMPO_API_KEY,
  features: 'all', // [!code focus]
})
```

<span id="balance-diffs" />

### Balance diffs

Set `features: 'all'` to simulate filled transactions and return token changes for the transaction sender in `capabilities.balanceDiffs`:

```ts [response.ts]
result.capabilities.balanceDiffs
// {
//   '0x1234567890abcdef1234567890abcdef12345678': [{
//     address: '0x20c000000000000000000000b9537d11c60e8b50',
//     decimals: 6,
//     direction: 'outgoing',
//     formatted: '100.000000',
//     name: 'USDC.e',
//     recipients: ['0xcafebabecafebabecafebabecafebabecafebabe'],
//     symbol: 'USDC.e',
//     value: '0x5f5e100',
//   }],
// }
```

Pass `capabilities.balanceDiffs: false` in a request when you need fee metadata but want to skip the balance simulation.

<span id="fee-derivation" />

### Fee estimates

With `features: 'all'`, the relay derives the transaction fee from the filled gas fields and resolved fee token. It returns raw token units alongside formatted metadata:

```ts [response.ts]
result.capabilities.fee
// {
//   amount: '0x6b86',
//   decimals: 6,
//   formatted: '0.027526',
//   symbol: 'USDC.e',
// }
```

<span id="require-funds" />

### Funding requirements

Set `capabilities.errors: true` to receive structured fill failures. For an `InsufficientBalance` error, the relay adds `capabilities.requireFunds` with the exact token deficit and returns optimistic balance diffs when available.

```ts twoslash
// @noErrors
import { Provider } from 'accounts'

const provider = Provider.create({ relay: 'http://localhost:3000' })
const [account] = await provider.request({ method: 'eth_requestAccounts' })
// ---cut---
const result = await provider.request({
  method: 'eth_fillTransaction',
  params: [{
    from: account,
    capabilities: { errors: true }, // [!code focus]
    calls: [{
      to: '0x20c000000000000000000000b9537d11c60e8b50',
      data: '0xa9059cbb000000000000000000000000cafebabecafebabecafebabecafebabecafebabe0000000000000000000000000000000000000000000000000000000005f5e100',
    }],
  }],
})

if (!('capabilities' in result))
  throw new Error('The relay did not return Tempo capabilities')

result.capabilities.requireFunds
// {
//   amount: '0x3938700',
//   decimals: 6,
//   formatted: '60.000000',
//   token: '0x20c000000000000000000000b9537d11c60e8b50',
//   symbol: 'USDC.e',
// }
```

<span id="enabling-features" />

### Enable all enrichment features

By default, the relay enables only the features you configure directly. Use `features: 'all'` to enable fee-token resolution, automatic swaps, balance simulation, and fee estimates together. This does not enable sponsorship; configure [`feePayer`](#feepayer) separately.

```ts
import { Handler } from 'tapimo/server'

const handler = Handler.relay({
  apiKey: process.env.TEMPO_API_KEY,
  features: 'all', // [!code focus]
})
```

This adds network requests for token balances and simulation, so enable the complete feature set only when your application uses the returned metadata.

## API Reference

<span id="apikey" />

### `apiKey`

* **Type:** `string`
* **Optional**

Routes the built-in mainnet and testnet clients through the Tempo API and authenticates verified-token requests. When you omit it, the clients use each chain's default Viem transport and verified-token requests are unauthenticated.

```ts
const handler = Handler.relay({
  apiKey: process.env.TEMPO_API_KEY, // [!code focus]
})
```

<span id="getclient" />

### `getClient`

* **Type:** `(chainId?: number) => Client`
* **Optional**

Overrides the built-in Viem clients for Tempo mainnet and testnet. With `apiKey`, the built-in clients use the Tempo API; without one, they use each chain's default Viem transport. Provide `getClient` for custom transports or additional chains. The relay calls it without an argument for the default client and with the request's resolved chain ID when available. For hosted Tempo RPC, use the built-in clients and set [`apiKey`](#apikey).

When you provide `getClient`, `apiKey` does not change the custom clients' transports. It still authenticates the built-in verified-token resolver unless you also provide `resolveTokens`.

<span id="autoswap" />

### `autoSwap`

* **Type:** `false | { slippage?: number }`
* **Optional**

Controls Stablecoin DEX swaps for insufficient balances. An options object enables automatic swaps. Set it to `false` to disable them when `features: 'all'` is enabled.

```ts
const handler = Handler.relay({
  autoSwap: { slippage: 0.02 }, // [!code focus]
})
```

<span id="autoswapslippage" />

#### `autoSwap.slippage`

* **Type:** `number`
* **Default:** `0.05` (5%)

Sets the maximum swap slippage. The relay calculates `maxAmountIn` from the token deficit and this percentage.

<span id="features" />

### `features`

* **Type:** `'all'`
* **Optional**

Set this to `'all'` to enable fee-token resolution, automatic swaps, balance diffs, and fee estimates. Fee-token candidates come from the Tempo API verified-token list by default. Passing `resolveTokens` also enables fee-token resolution and replaces the default candidates. Configure `feePayer` separately to enable sponsorship.

```ts
const handler = Handler.relay({
  features: 'all', // [!code focus]
})
```

<span id="feepayer" />

### `feePayer`

* **Type:** `object`
* **Optional**

Configures transaction sponsorship. When present, the relay can sign the filled transaction's `feePayerSignature`.

```ts
import { Handler } from 'tapimo/server'
import { Account } from 'viem/tempo'

const handler = Handler.relay({
  feePayer: { // [!code focus]
    account: Account.fromSecp256k1('0x...'), // [!code focus]
    feeToken: '0x20c000000000000000000000b9537d11c60e8b50', // [!code focus]
    name: 'My App', // [!code focus]
    url: 'https://myapp.com', // [!code focus]
  }, // [!code focus]
})
```

<span id="feepayeraccount" />

#### `feePayer.account`

* **Type:** `LocalAccount`
* **Required**

The local account that signs fee payer payloads.

<span id="feepayerfeetoken" />

#### `feePayer.feeToken`

* **Type:** `Address`
* **Optional**

The token the sponsor prefers to use for fees. This value overrides the transaction sender's requested `feeToken` during sponsorship.

<span id="feepayeronsponsored" />

#### `feePayer.onSponsored`

* **Type:** `(event: SponsoredEvent) => void | Promise<void>`
* **Optional**

Runs after fee-payer signing and before the relay returns or broadcasts the transaction. The awaited event includes the chain ID, method, sender, signing payload, serialized transaction, and transaction hash when available.

<span id="feepayername" />

#### `feePayer.name`

* **Type:** `string`
* **Optional**

A sponsor name returned in response metadata.

<span id="feepayerurl" />

#### `feePayer.url`

* **Type:** `string`
* **Optional**

A sponsor URL returned in response metadata.

<span id="feepayervalidate" />

#### `feePayer.validate`

* **Type:** `(request: TransactionRequest & { chainId?: number | Hex }) => Validation | Promise<Validation>`
* **Optional**

Approves or rejects sponsorship for a Tempo transaction. Return `false` or a named refusal reason to reject sponsorship. Named reasons are `billing_past_due`, `billing_required`, `fee_token_unsupported`, `spend_limit_exceeded`, and `tx_fee_limit_exceeded`. When omitted, the configured fee payer sponsors every eligible request.

```ts
import { Handler } from 'tapimo/server'
import { isAddressEqual } from 'viem'
import { Account } from 'viem/tempo'

const allowedSender = '0x000000000000000000000000000000000000dead'

const handler = Handler.relay({
  feePayer: {
    account: Account.fromSecp256k1('0x...'),
    validate: (request) => // [!code focus]
      request.from !== undefined && isAddressEqual(request.from, allowedSender), // [!code focus]
  },
})
```

<span id="multisig" />

### `multisig`

* **Type:** `Multisig.Options`
* **Optional**

Reserved for native multisig support in configurable accounts, which is not live. This package option is documented for reference and is not part of the current production integration path.

For current account authorization, see [Accounts](https://tempo.xyz/developers/docs/accounts).

<span id="cache" />

### `cache`

* **Type:** `Store.Store`
* **Optional**

Caches TIP-20 metadata and account fee-token preferences. When omitted, the relay reads this data upstream on every fill.

```ts
import { Handler, Store } from 'tapimo/server'

const handler = Handler.relay({
  cache: Store.memory(), // [!code focus]
})
```

<span id="onrequest" />

### `onRequest`

* **Type:** `(request: RpcRequest) => Promise<void>`
* **Optional**

Runs before the relay handles each request. Use it for logging, rate limits, or request-level validation.

```ts
const handler = Handler.relay({
  onRequest: async (request) => { // [!code focus]
    console.log('Processing request:', request.method) // [!code focus]
  }, // [!code focus]
})
```

<span id="path" />

### `path`

* **Type:** `string`
* **Default:** `'/'`

Sets the route where the handler accepts requests.

```ts
const handler = Handler.relay({
  path: '/relay', // [!code focus]
})
```

When you use [`Provider.relay`](https://accounts.tempo.xyz/docs/api/provider#relay), route both the base path and its chain-qualified children to the handler. The provider sends JSON-RPC requests to `/relay/:chainId`.

<span id="resolvetokens" />

### `resolveTokens`

* **Type:** `(chainId: number) => readonly Address[] | Promise<readonly Address[]>`
* **Optional**

Returns candidate token addresses for fee-token resolution. By default, the relay loads the [Tempo API verified-token list](https://tempo.xyz/developers/docs/api/verified-tokens#gettokenlist) for mainnet and testnet. Pass a resolver to replace these candidates. For additional chains, provide both `getClient` and `resolveTokens`.

```ts
const handler = Handler.relay({
  resolveTokens: () => [ // [!code focus]
    '0x20c0000000000000000000006a37da5c996874be', // [!code focus]
    '0x20c000000000000000000000b9537d11c60e8b50', // [!code focus]
  ], // [!code focus]
})
```

A custom resolver replaces only token lookup. `apiKey` still configures the built-in RPC clients.

<span id="cors" />

### `cors`

* **Type:** `boolean | Handler.from.Cors`
* **Default:** `true`

Configures CORS headers for every response. This option is inherited from `Handler.from`.

<span id="headers" />

### `headers`

* **Type:** `Headers | Record<string, string>`
* **Optional**

Adds headers to every response. This option is inherited from `Handler.from`.
