> ## Documentation Index
> Fetch the complete documentation index at: https://docs.privy.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Receive policies

> Control which tokens and senders a Privy wallet accepts on Tempo, and claim payments that Tempo holds.

A Tempo [receive policy](https://tempo.xyz/developers/docs/accounts/policies/receive-policies) controls which stablecoins a wallet accepts and who can pay it. Tempo doesn't revert a payment that breaks the policy. The transfer succeeds, but Tempo holds the funds in the `ReceivePolicyGuard` contract instead of crediting the wallet. Only the wallet's recovery authority can release or return them.

Apps use receive policies to keep address poisoning, sanction dusting, and spam tokens out of user wallets.

## How receive policies work

Each wallet's receive policy has three parts.

| Part | Decides | Values |
| - | - | - |
| Token filter | Which stablecoins the wallet accepts. | `1n` (allow all), `0n` (reject all), or a list of token addresses. |
| Sender policy | Who can pay the wallet. | `1n` (allow all), `0n` (reject all), or an allowlist or denylist of senders. |
| Recovery authority | Who can release or return held payments. | `address(0)` (default) lets each payment's sender reclaim it. Or any address, such as your recovery wallet. |

Lists live in Tempo's TIP-403 registry, and each list has one admin. Many wallets can share a list, so one update applies to every wallet that uses it.

## Choose a setup

Choose by what your wallets receive.

| Your wallets receive | Token filter | Sender policy | Recovery authority |
| - | - | - | - |
| Only deposits from known sources, such as exchange or on-ramp deposit wallets. | Supported stablecoins | Allowlist of trusted senders, such as treasury wallets, on-ramps, exchanges, and bridges. | `address(0)`, or your recovery wallet to review and credit held payments yourself |
| Payments from anyone, such as consumer or P2P wallets. | Supported stablecoins | `1n` (allow all), or a denylist of known bad actors. | `address(0)` |
| Payments the user manages, such as self-custody wallets. | Supported stablecoins | `1n` (allow all). | The wallet itself |

Attackers create new addresses for free, so only an allowlist stops address poisoning. An allowlist also holds payments from everyone not on it, including your other users, so use one only for wallets that receive from known sources, and keep it current as exchanges rotate their wallets.

To stop all incoming payments to a flagged wallet, set its token filter and sender policy to `0n`. The wallet can still send.

## Prerequisites

* An admin wallet: a Privy wallet your app controls. It administers the shared lists.
* Optional: a recovery wallet, a Privy wallet your app controls and protects with a [key quorum](https://docs.privy.io/controls/key-quorum/overview.md). You need one only if your app, not the sender, decides what happens to held payments. Keep it separate from the admin wallet, so one leaked key can't both change who pays your users and move held funds.
* [Gas sponsorship](https://docs.privy.io/wallets/gas-and-asset-management/gas/setup.md) turned on for Tempo. To pay fees from the wallet instead, drop `sponsor` and set `fee_token` in the transaction.
* `viem` 2.57 or later, which exports the TIP-403 helpers from `viem/tempo`.

Each step builds a call with a `viem/tempo` helper and sends its `to` and `data` from a Privy wallet as a Tempo transaction (type `118`). Pass only `to` and `data`: the helpers also return fields that Privy rejects. The examples use Tempo mainnet (`eip155:4217`). For Tempo Moderato (testnet), use `eip155:42431`.

## 1. Create the shared lists

Create each list once, from the admin wallet. The admin wallet becomes the list admin, so only it can edit the list.

Start with the token list: the stablecoins the wallets accept. viem names an allowlist `'whitelist'` and a denylist `'blacklist'`, after the TIP-403 policy types.

```typescript {skip-check} theme={"system"}
import {PrivyClient} from '@privy-io/node';
import {createPublicClient, http, zeroAddress, type Hex} from 'viem';
import {tempo} from 'viem/chains';
import {Actions} from 'viem/tempo';

const privy = new PrivyClient({
  appId: process.env.PRIVY_APP_ID,
  appSecret: process.env.PRIVY_APP_SECRET
});
const publicClient = createPublicClient({chain: tempo, transport: http()});

const adminWalletId = 'insert-admin-wallet-id';
const adminWalletAddress = 'insert-admin-wallet-address';
const USDC_E = '0x20c000000000000000000000b9537d11c60e8b50';

const {to, data} = Actions.policy.create.call({
  admin: adminWalletAddress,
  type: 'whitelist',
  addresses: [USDC_E]
});

const {hash} = await privy
  .wallets()
  .ethereum()
  .sendTransaction(adminWalletId, {
    caip2: 'eip155:4217',
    params: {transaction: {type: 118, calls: [{to, data}]}},
    sponsor: true
  });

const receipt = await publicClient.waitForTransactionReceipt({hash: hash as Hex});
const tokenPolicyId = Actions.policy.create.extractEvent(receipt.logs).args.policyId;
```

Then create the sender list the same way, with the addresses allowed to pay user wallets. Read `senderPolicyId` from its `PolicyCreated` event.

```typescript {skip-check} theme={"system"}
const {to, data} = Actions.policy.create.call({
  admin: adminWalletAddress,
  type: 'whitelist', // or 'blacklist' for a denylist of specific senders
  addresses: [treasuryAddress, onrampAddress, exchangeAddress, bridgeAddress]
});
```

Save both IDs in your config. To accept any sender, skip the sender list and use policy `1n`.

## 2. Set the policy on each wallet

Send one transaction from each user wallet in the same flow that creates it. A wallet without a policy accepts every payment, and funds can arrive before its first transaction. This example points the wallet at both shared lists and sets the recovery authority to `address(0)`, so each sender can reclaim its own held payment. This is the default: your app doesn't hold anyone's funds, and senders can fix a mistaken payment themselves.

```typescript {skip-check} theme={"system"}
const {to, data} = Actions.receivePolicy.set.call({
  senderPolicyId,
  tokenFilterId: tokenPolicyId,
  recoveryAuthority: zeroAddress // address(0): each sender reclaims its own payment
});

await privy
  .wallets()
  .ethereum()
  .sendTransaction(userWalletId, {
    caip2: 'eip155:4217',
    params: {transaction: {type: 118, calls: [{to, data}]}},
    sponsor: true
  });
```

To have your app review held payments and credit or return them, set `recoveryAuthority` to your recovery wallet's address instead. `recoveryAuthority` has no default in `.call()`, so always set it.

Then read the policy back to confirm it's set:

```typescript {skip-check} theme={"system"}
const policy = await Actions.receivePolicy.get(publicClient, {account: userWalletAddress});
// policy.hasReceivePolicy === true
// policy.claimer === 'sender'
```

`get` returns built-in values by name: `'allow-all'` for `1n`, `'reject-all'` for `0n`, and a claimer of `'sender'` for `address(0)` or `'self'` for the wallet itself.

If a user owns the wallet, pass their JWT in `authorization_context`, as for any other request from that wallet.

### Lock the policy

Any signer of the wallet can call `setReceivePolicy` again and switch back to allow-all. To stop that, attach a [Privy policy](https://docs.privy.io/controls/policies/overview.md) that denies calls to the TIP-403 registry and allows everything else. Privy checks every call in a type `118` transaction, so the registry call can't hide inside a batch.

```typescript {skip-check} theme={"system"}
{
  version: '1.0',
  name: 'Lock receive policy',
  chain_type: 'ethereum',
  rules: [
    {
      name: 'Receive policy is managed by the app',
      method: 'eth_sendTransaction',
      conditions: [
        {
          field_source: 'ethereum_transaction',
          field: 'to',
          operator: 'eq',
          value: Addresses.tip403Registry
        }
      ],
      action: 'DENY'
    },
    {
      name: 'Allow other Tempo transactions',
      method: 'eth_sendTransaction',
      conditions: [
        {field_source: 'ethereum_transaction', field: 'chain_id', operator: 'eq', value: '4217'}
      ],
      action: 'ALLOW'
    }
  ]
}
```

A user who exports their key can still change the policy outside Privy.

> [!NOTE]
>
> Virtual addresses use their master address's receive policy and can't set their own.

## 3. Update the sender list

Add or remove a sender from the admin wallet. Every wallet that uses the list picks up the change at once.

```typescript {skip-check} theme={"system"}
const {to, data} = Actions.policy.modifyWhitelist.call({
  policyId: senderPolicyId,
  address: newSenderAddress,
  allowed: true // false removes the sender
});
```

Send the call from `adminWalletId` as in step 1. For a denylist, use `Actions.policy.modifyBlacklist.call` with `restricted` instead of `allowed`. To see who's on a list, call Tempo's [policy members](https://tempo.xyz/developers/docs/api/policies) endpoint.

## 4. Review blocked payments

Each blocked payment emits `TransferBlocked` from `ReceivePolicyGuard`. Blocked payments can't be listed onchain, so your app must index these events. Read them from the last block you processed, so none are missed while your service is down, and read `ReceiptClaimed` and `ReceiptBurned` the same way to mark payments released.

```typescript {skip-check} theme={"system"}
import {Abis, Addresses} from 'viem/tempo';

const blocked = await publicClient.getContractEvents({
  address: Addresses.receivePolicyGuard,
  abi: Abis.receivePolicyGuard,
  eventName: 'TransferBlocked',
  args: {receiver: userWalletAddress},
  fromBlock: lastProcessedBlock + 1n
});

for (const {args} of blocked) {
  const {token, amount, blockedNonce, receipt: claimReceipt} = args;
  // Store the payment for review, keyed by token and blockedNonce
}
```

With `address(0)` as recovery authority, the sender reclaims a held payment from its own wallet, sending the funds back to itself or to another address it chooses. With a recovery wallet, your app claims it instead: set `to` to the user's wallet to credit it, or to another address to return it.

```typescript {skip-check} theme={"system"}
const {to, data} = Actions.receivePolicy.claim.call({
  to: senderAddress, // or userWalletAddress, when your recovery wallet credits it
  receipt: claimReceipt
});
```

Send the call from the sender's wallet, or from `recoveryWalletId`, as in step 1. A claim from any other wallet reverts, and so does a second claim of the same payment. Leave sanction dust held for compliance review.

## Things to know

> [!WARNING]
>
> A blocked payment still emits a `Transfer` event, with `ReceivePolicyGuard` as the recipient.
> Credit a deposit only when the confirmed `Transfer` event names the wallet itself.

* **Test on Moderato first.** `Actions.receivePolicy.validate(publicClient, {token, sender, receiver})` shows whether the policy accepts a payment, and why not (`tokenFilter` or `receivePolicy`). It works on mainnet too.
* **A claim releases the full amount.** To check a payment's amount first, call `Actions.receivePolicy.getBlockedBalance(publicClient, {receipt})`.
* **A claim to the receiving wallet skips the receive policy.** Only the token's own policy applies, so claim only approved payments.
* **A new recovery authority applies only to later payments.** Payments already blocked keep their original authority.

## Related resources

> [card] [Using Tempo with Privy](https://docs.privy.io/recipes/tempo/send-transactions.md)
>
> Create wallets and send transactions on Tempo

> [card] [Tempo policy examples](https://docs.privy.io/controls/policies/example-policies/tempo.md)
>
> Control fee tokens, sponsorship, and validity windows with Privy policies

> [card] [Tempo receive policies](https://tempo.xyz/developers/docs/accounts/policies/receive-policies)
>
> Tempo's reference for receive policies and the TIP-403 registry

> [card] [Tempo policies API](https://tempo.xyz/developers/docs/api/policies)
>
> List the members of an allowlist or denylist

> [card] [Balance webhooks](https://docs.privy.io/wallets/gas-and-asset-management/assets/balance-event-webhooks.md)
>
> Subscribe to deposit and withdrawal events
