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.
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.
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. 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 turned on for Tempo. To pay fees from the wallet instead, drop
sponsorand setfee_tokenin the transaction. viem2.57 or later, which exports the TIP-403 helpers fromviem/tempo.
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.
senderPolicyId from its PolicyCreated event.
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 toaddress(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.
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:
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 callsetReceivePolicy again and switch back to allow-all. To stop that, attach a Privy policy 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.
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.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 endpoint.
4. Review blocked payments
Each blocked payment emitsTransferBlocked 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.
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.
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
- Test on Moderato first.
Actions.receivePolicy.validate(publicClient, {token, sender, receiver})shows whether the policy accepts a payment, and why not (tokenFilterorreceivePolicy). 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
Using Tempo with Privy
Create wallets and send transactions on Tempo
Tempo policy examples
Control fee tokens, sponsorship, and validity windows with Privy policies
Tempo receive policies
Tempo’s reference for receive policies and the TIP-403 registry
Tempo policies API
List the members of an allowlist or denylist
Balance webhooks
Subscribe to deposit and withdrawal events

