Privy XRPL wallets for AI agents: what the policy checks, and what it lets through.
Update, 22 October 2026: Privy changed its denylist example the day after we reported the gap. Its fallback rule is now "Allow other Payments", limited to TransactionType Payment, with a new warning: "A Payment.Destination denylist only screens Payment transactions. … Never pair a denylist with an unconditional ALLOW." Thanks to the Privy team. Policies copied from the old example still have the gap described below, so check yours.
On 13 October Privy and Ripple announced XRP Ledger wallets in Privy: an app or an agent creates an XRPL wallet, builds a transaction with xrpl.js, and Privy signs it with xrpl_signTransaction. Privy's policy engine decides what each wallet may sign. For an agent that pays x402 sellers in RLUSD, that policy is the guard between the agent and its funds.
This guide says what the policy can read, where Privy's own sanctions denylist example leaves a gap, a tighter policy for x402 payments, and how to check the transactions a policy can't read before they are signed.
What a Privy XRPL policy reads
Privy's documentation: "The policy engine inspects fields on these transaction types: Payment, OfferCreate, OfferCancel, and TrustSet." A rule can match their fields, such as Payment.Destination or Payment.Amount.value.
Every other type has no fields a rule can match. Only a rule without transaction conditions decides it: an empty condition list, or a system condition such as the time. A request that no rule allows is refused, which is the safe default. raw_sign requests have no decoded transaction at all, so their rules can only use system conditions.
The gap in the old denylist example
Until 22 October, Privy's example "Denylist Payment destinations" had two rules for xrpl_signTransaction: deny a Payment whose Payment.Destination is in a condition set of sanctioned addresses, then "Allow all other requests" with an empty condition list. Privy's note explained it: "every other request falls through to the ALLOW rule." A policy copied from it still works this way.
So that denylist stops one transaction type. The same value can reach the same address in other ways, and the account itself can be handed over:
| Transaction | What it does | Old denylist example | presign-guard |
|---|---|---|---|
Payment to a sanctioned address | Pays it | Refused | red · SANCTIONED_ADDRESS |
CheckCreate to that address | A check it can cash | Signed | red · SANCTIONED_ADDRESS, XRPL_CHECK_TO_OTHER |
EscrowCreate to that address | Funds it receives later | Signed | red · SANCTIONED_ADDRESS, XRPL_ESCROW_TO_OTHER |
PaymentChannelCreate to that address | A channel it can claim from | Signed | red · SANCTIONED_ADDRESS, XRPL_CHANNEL_TO_OTHER |
Batch with that Payment inside | The outer type is Batch, so no Payment.Destination to match | Signed | red · SANCTIONED_ADDRESS (inner transaction) |
AccountDelete | Sends all remaining XRP to the destination | Signed | red · XRPL_ACCOUNT_DELETE |
SetRegularKey to another key | That key can sign for the account | Signed | red · XRPL_REGULAR_KEY_CHANGE |
SignerListSet with other signers | They can sign for the account | Signed | red · XRPL_SIGNER_LIST_CHANGE |
DelegateSet with Payment | Another account can pay itself from yours | Signed | red · XRPL_DELEGATE_FUNDS |
The "Old denylist example" column follows the rule order Privy documented until 22 October; we did not send these through a Privy app. With Privy's updated example, all of them except the Payment fall to the default refusal. The presign-guard column is the real check, run on 21 October against the mainnet with an address from the OFAC SDN list.
An agent rarely asks for these on its own. It does when a prompt injection, a malicious tool result or a seller's instructions tell it to, which is exactly the case a policy is there for.
A tighter policy for an agent that pays x402 sellers in RLUSD
Allow only what the agent needs, deny sanctioned destinations, and let everything else fall to Privy's default refusal:
{
"version": "1.0",
"name": "x402 on the XRP Ledger: RLUSD only, capped, never to a sanctioned address",
"chain_type": "xrpl",
"rules": [
{
"name": "Never pay a sanctioned address",
"method": "xrpl_signTransaction",
"conditions": [
{ "field_source": "xrpl_transaction", "field": "Payment.Destination", "operator": "in_condition_set", "value": "<PRIVY_XRPL_SET_ID>" }
],
"action": "DENY"
},
{
"name": "RLUSD from Ripple's issuer, at most 1 per payment",
"method": "xrpl_signTransaction",
"conditions": [
{ "field_source": "xrpl_transaction", "field": "TransactionType", "operator": "eq", "value": "Payment" },
{ "field_source": "xrpl_transaction", "field": "Payment.Amount.currency", "operator": "eq", "value": "524C555344000000000000000000000000000000" },
{ "field_source": "xrpl_transaction", "field": "Payment.Amount.issuer", "operator": "eq", "value": "rMxCKbEDwqr76QuheSUMdEGf4B9xJ8m5De" },
{ "field_source": "xrpl_transaction", "field": "Payment.Amount.value", "operator": "lte", "value": "1" }
],
"action": "ALLOW"
},
{
"name": "Hold RLUSD: a trust line to Ripple's issuer only",
"method": "xrpl_signTransaction",
"conditions": [
{ "field_source": "xrpl_transaction", "field": "TransactionType", "operator": "eq", "value": "TrustSet" },
{ "field_source": "xrpl_transaction", "field": "TrustSet.LimitAmount.currency", "operator": "eq", "value": "524C555344000000000000000000000000000000" },
{ "field_source": "xrpl_transaction", "field": "TrustSet.LimitAmount.issuer", "operator": "eq", "value": "rMxCKbEDwqr76QuheSUMdEGf4B9xJ8m5De" }
],
"action": "ALLOW"
}
]
}
- No rule without conditions. Checks, escrows, channels, Batches, key changes, signer lists, deletes and delegations match no ALLOW rule, so Privy refuses them.
- RLUSD's currency code on the ledger is the hex
524C5553…, not the letters "RLUSD". x402 offers for RLUSD onxrpl:0carry the same code as theirasset. - The cap: set
Payment.Amount.valueto what one call may cost. - Keep
raw_signrefused: no rule allows it here, so it is. - The condition set holds classic r-addresses only and can't match destination tags. privy-sanctions-sync fills it from the OFAC SDN list and keeps it in step every day.
When the agent needs more: check before signing
Some agents do need other types, for example a DEX order or an escrow. Then the policy can't judge them, and the check has to happen before xrpl_signTransaction is called. presign-guard reads every XRPL transaction type in the table above, every inner transaction of a Batch, and screens every destination, new key, signer and delegate against the OFAC SDN list. With Privy's Node SDK:
import { encodeForSigning } from "xrpl";
// prepared = await xrpl.autofill({ TransactionType: ..., Account: walletAddress, ... })
// payFetch: any fetch that pays x402, e.g. createSafeFetch from x402-safe-fetch
const res = await payFetch("https://presign-guard.fizzl.eu/v1/check", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ type: "xrpl", network: "xrpl:0", tx: prepared }),
});
const { verdict, reasons } = await res.json();
if (verdict !== "green") {
// red: don't sign. orange: show the reasons to a person first.
throw new Error(`presign-guard ${verdict}: ${reasons.map((r) => r.code).join(", ")}`);
}
const { data } = await privy.wallets().rpc(walletId, {
method: "xrpl_signTransaction",
params: { transaction: encodeForSigning({ ...prepared, SigningPubKey: walletPublicKey }), encoding: "hex" },
});
The full check costs $0.01, paid over x402 in RLUSD on the XRP Ledger or USDC on Base, Solana, Algorand or Stellar. A failed check is never charged and never comes back green. For the verdict only, free and limited per hour, an agent can call presign_quick_check on presign-guard's MCP server (https://presign-guard.fizzl.eu/mcp) with the same {"type": "xrpl", "tx": {…}}.
Before paying a seller, x402 Doctor's preflight also checks the seller's XRPL side: whether the payout account exists, has the RLUSD trust line, accepts the payment, and is not on the sanctions list.
Questions
Which XRPL transaction types can a Privy policy read?
Payment, OfferCreate, OfferCancel and TrustSet, through the xrpl_transaction field source. Other types have no fields a rule can match: only a rule without transaction conditions (an empty list or a system condition such as the time) decides them. A request that no rule allows is refused.
Does Privy's XRPL denylist example stop payments to a sanctioned address?
Since 22 October 2026, yes: its fallback ALLOW now covers only Payment, so every other transaction type falls to Privy's default refusal, and a warning names the cases a destination denylist misses. Before that, its second rule, "Allow all other requests", allowed every other signing request, so a CheckCreate, EscrowCreate or PaymentChannelCreate to the same address, a Batch with that Payment inside, SetRegularKey, SignerListSet, AccountDelete and DelegateSet were all signed. Policies copied from the old example still do that: check yours.
What policy should an agent that pays x402 sellers in RLUSD use?
Allow only what it needs: a Payment in RLUSD from Ripple's issuer up to a cap, and a TrustSet to that issuer; deny Payments to a condition set of sanctioned addresses; let everything else fall to Privy's default refusal. Keep raw_sign refused. The privy-sanctions-sync example in Fizzl13/x402-examples keeps the condition set in step with the OFAC SDN list.
How do I check an XRPL transaction that the policy can't read?
Send it to presign-guard before calling xrpl_signTransaction: POST https://presign-guard.fizzl.eu/v1/check with {"type": "xrpl", "network": "xrpl:0", "tx": {…}} ($0.01 over x402), or the free presign_quick_check tool on presign-guard's MCP server for the verdict only. It judges every type above, every inner transaction of a Batch, and screens every destination, new key, signer and delegate against the OFAC SDN list. Sign only on green.
Links
- Privy: XRPL wallets and XRPL policy examples
- privy-sanctions-sync: Privy condition sets kept in step with the OFAC SDN list, for EVM, Solana and the XRP Ledger
- XRPL Batch, delegation, MPTs, vaults and loans: what each of the newer types can hide
- presign-guard on GitHub: every XRPL reason code