Skip to content
Pilot

Policy & delegation

Delegation, in one paragraph

Your budget account grants a Pilot-held delegate account permission to send specific transaction types on its behalf, using XRPL's native DelegateSet transaction (XLS-75 Permission Delegation). The grant is on-ledger, visible to anyone, limited to at most 10 permissions, and revocable by you at any time with an empty-permission DelegateSet.

Delegation operations

OperationWhat it does
account.link({ owner, network })Create or reuse a Testnet/Devnet delegate account and prepare the unsigned DelegateSet grant for you to sign.
account.status({ owner, network })Read the on-ledger delegation and refresh the link.
account.revoke({ owner, network })Prepare an empty-permission DelegateSet and suspend execution immediately until the revoke validates.

Permissions

SetTransaction types
Granted by default (7)OfferCreate, OfferCancel, TrustSet, AMMDeposit, AMMWithdraw, EscrowFinish, CheckCash
Delegable with a custom grantthe default 7, plus EscrowCancel, CheckCancel
Never delegatedPayment, EscrowCreate, CheckCreate, PaymentChannelCreate, PaymentChannelFund, NFTokenCreateOffer, PaymentBurn, PaymentMint
Non-delegable (owner only)AccountSet, SetRegularKey, SignerListSet, DelegateSet, AccountDelete, Batch, all Vault*, Loan*, LoanBroker*, and network-level types

On a Ledger, the grant shows each permission as its XLS-75 value (transaction type code + 1): 8, 9, 21, 37, 38, 3, 18. See the legend.

Policy rules

Every tx.execute evaluates these rules in order:

#RuleOn failure
1network: in policy.networks; Mainnet always failsblock
2link_active: the account is linked and the link is activeblock (revoked link → needs approval)
3transaction_type: in allowed_tx_types and granted on-ledgerneeds approval
4issuer_allowlist: every issued asset's issuer is allowlistedblock
5require_auth: no RequireAuth issuers (when enabled)block
6preflight: all preflight checks passblock
7simulation: tesSUCCESSblock
8slippage: estimate ≤ max_slippage_bpsblock
9max_per_tx_usdblock
10daily_cap_usd: today's total + this transactionblock
11execute_under_usd: value strictly belowneeds approval

Verdict: block if any rule blocks; else needs_approval if any rule asks for approval; else execute.

Default policy

{
  max_per_tx_usd: 2000,
  daily_cap_usd: 5000,
  execute_under_usd: 250,          // auto-execute only below this
  max_slippage_bps: 100,           // 1%
  allowed_tx_types: ["OfferCreate", "OfferCancel", "TrustSet",
                     "AMMDeposit", "AMMWithdraw", "EscrowFinish", "CheckCash"],
  issuer_allowlist: ["RLUSD"],     // resolves to the per-network RLUSD issuer
  block_requireauth_issuers: true,
  networks: ["testnet", "devnet"],
  x402_max_per_call_drops: "10000",
  x402_daily_cap_drops: "1000000",
  ap4m_enabled: false
}

"RLUSD" resolves to rQhWct2fv4Vc4KRjRgMrxa8xPN9Zx9iLKV on Testnet.

Changing policy

await harbor.policy.set({
  owner: "r...",
  network: "testnet",
  policy: {
    max_per_tx_usd: 500, daily_cap_usd: 1000, execute_under_usd: 50,
    max_slippage_bps: 50,
    allowed_tx_types: ["OfferCreate", "OfferCancel"],
    issuer_allowlist: ["RLUSD"], block_requireauth_issuers: true,
    networks: ["testnet"],
    x402_max_per_call_drops: "10000", x402_daily_cap_drops: "1000000",
    ap4m_enabled: false
  }
});

Invariants: execute_under_usd ≤ max_per_tx_usd ≤ daily_cap_usd; x402 per-call ≤ daily; lists unique; networks cannot contain mainnet. In the Pilot app, editing Pilot/Policy.md on the Memory page syncs to policy.set.

Watcher

When a cron trigger is configured (designed for * * * * *), the engine pages each active link's validated transactions. No checked-in deployment config enables the trigger yet, and the read-only and trading local profiles skip it. Any transaction signed by the delegate that the engine did not execute is foreign: the link is suspended and an unsigned revoke is prepared for the owner. With QuickNode enabled, a Streams watchdog does the same from every validated ledger (QuickNode).