Repository navigation
Amendment Idea: Spending Limits #675
BraedonKlock
started this conversation in
XLS Ideas (pre standard proposal)
Replies: 1 comment 1 reply
|
I would think about moving it from "Delegate" because one could just delegate and add this in a batch. That way those who don't delegate can still use the feature. I think thats why we put it on firewall. |
1 reply
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Spending Limits
Update (November 1, 2026): I've updated the proposal based on feedback and further discussions with @dangell7
Names, fields, and rules are tentative. Anything marked Open is undecided, and feedback on those points is especially welcome. See Section 9: Open Questions.
1. Abstract
Spending Limits let an account owner control how a specific asset can be spent, by which transaction type, and by whom. A limit can restrict:
Limits are configured per asset and per transaction type. They apply to either the owner's own transactions (account-level) or to one delegate's transactions (delegate-specific). Only the limits belonging to whoever submits the transaction are enforced.
To stop limits from being bypassed through a different transaction type, once a scope has any limit, asset-moving transactions without their own limit for each asset they spend are rejected. Owners must explicitly configure every transaction type they want to allow.
A new
SpendingLimitSettransaction manages limits, and each limit is stored in its ownSpendingLimitledger entry.2. Motivation
My main motivation is autonomous agents, particularly AI agents.
An AI agent doesn't have to be compromised to make a mistake. It can hallucinate, misunderstand an instruction, or be confident in a decision that is wrong.
Today, if an owner gives an agent permission to submit payments through Permission Delegation, there's no way to restrict:
Any such limits have to live in the agent's own application logic, which is exactly the component we're trying to guard against. This proposal moves those limits into the protocol, where the agent can't skip them.
Agents also need to do more than send payments. An agent might pay invoices, trade on the DEX, and lock funds in escrow, and the owner will want different limits for each. A single limit per asset can't express that, so limits are configured per transaction type.
The feature is also useful without delegation, for example when an agent operates an account with the account's own keys and the owner wants a safety net against mistakes.
3. Specification
3.1. Concepts
3.1.1. Scopes
SpendingLimitSetwithoutDelegatefieldSpendingLimitSetwithDelegatefieldOnly the submitter's limits are enforced:
This lets an owner restrict delegates without restricting themselves, and give each delegate different limits.
3.1.2. Limit identity
A limit is indexed by:
So
Alice + Bob + Payment + XRPandAlice + Bob + EscrowCreate + XRPare two separate limits, each with its own restrictions and usage.3.1.3. Restrictions
Each limit may specify any combination of:
3.2. Enforcement
When a transaction is submitted:
Transaction types that don't spend an asset (for example
AccountSet,SignerListSet, orSpendingLimitSetitself) are never affected by limits.3.3. Preventing Bypass: Deny by Default
The problem: if Alice gives Bob both Payment and OfferCreate permissions but limits only his XRP payments, Bob could move XRP through an offer instead. The payment limit works, but it doesn't give Alice the protection she expects.
The rule: once a scope (the owner, or a specific delegate) has at least one limit (for any asset), every asset-moving transaction in that scope must have its own limit for its transaction type and each asset it spends, or it is rejected.
Example: Alice gives Bob Payment, EscrowCreate, and OfferCreate permissions, and configures XRP limits for Bob's Payment and EscrowCreate only.
The only way to bypass a limit is for the owner to explicitly configure a limit for another transaction type.
3.3.1. Tracking whether a scope has limits
Limits are keyed by transaction type, so a single lookup can't distinguish "no limits anywhere in this scope" from "this transaction type and asset aren't configured, but other limits exist." Checking every possible transaction type and asset would be expensive.
The proposed solution is an active-limit indicator: a counter, maintained by
SpendingLimitSetas limits are created and removed, recording how many limits a scope has. Enforcement then needs at most two direct lookups: the indicator and the specific limit.3.3.2. Allowing a transaction type without restrictions
Under deny-by-default, once a scope has any limit, every transaction type and asset combination without its own limit is rejected. An owner therefore needs a way to allow a combination without limiting it.
The proposed approach is a limit that specifies no restrictions: none of
MaxAmount,MaxTransactions, orMinInterval. Such a limit means "this transaction type and asset are explicitly allowed, with no restrictions."Example: Alice wants to limit Bob's XRP payments to 100 XRP, but let him pay RLUSD freely.
Bob + Payment + XRPwithMaxAmountof 100 XRP.Bob + Payment + RLUSDlimit.Bob + Payment + RLUSDwith no restrictions.3.4. Measuring Spending
Transaction types don't share a single amount field. Payment uses
Amount(andSendMaxfor cross-currency payments), OfferCreate usesTakerGetsandTakerPays, and others have their own fields and logic.The proposed approach:
doApply()logic.3.5. Ledger Entry:
SpendingLimit3.5.1. Object Identifier
A
SpendingLimitis indexed by:Owner,LimitedTransactionType, andAsset.Owner,Delegate,LimitedTransactionType, andAsset.The two forms must be separated so they can never collide.
3.5.2. Fields
OwnerDelegateLimitedTransactionTypeAssetMaxAmountMaxTransactionsMinIntervalTransactionCountLastTransactionTimeOwnerNodePreviousTxnIDPreviousTxnLgrSeqA limit with none of
MaxAmount,MaxTransactions, orMinIntervalmeans "allowed, unlimited" (Section 3.3.2).3.5.3. Ownership and Reserves
Owner. The entry is placed in the owner's directory.SpendingLimit.The delegate does not own the entry and pays no reserve.
3.5.4. Deletion
SpendingLimitSet(removal operation).SpendingLimitentries hold no funds and only restrict the owner's own account, so, likeDelegateandDepositPreauthentries, they are deleted along with the owner's account.3.5.5. Invariants
TransactionCountnever decreases, except through an explicit owner update (if editing resets usage, see Q6).MaxTransactionsis present, it is greater than 0, andTransactionCount <= MaxTransactions.Owner != Delegate.MaxAmount, if present, is positive and denominated inAsset.3.5.6. Example JSON
{ "LedgerEntryType": "SpendingLimit", "Flags": 0, "Owner": "rAlice...", "Delegate": "rBob...", "LimitedTransactionType": "Payment", "Asset": { "currency": "XRP" }, "MaxAmount": "100000000", "MaxTransactions": 5, "MinInterval": 86400, "TransactionCount": 2, "LastTransactionTime": 815097600, "OwnerNode": "0000000000000000", "PreviousTxnID": "...", "PreviousTxnLgrSeq": 12345678 }3.6. Transaction:
SpendingLimitSetCreates, modifies, or removes a single limit.
3.6.1. Fields
TransactionTypeSpendingLimitSetAccountDelegateLimitedTransactionTypeAssetMaxAmountMaxTransactionsMinIntervalThe limit's transaction type uses its own field,
LimitedTransactionType, becauseTransactionTypeidentifies the transaction itself (SpendingLimitSet).Behavior:
Account(which becomes the limit'sOwner),Delegate,LimitedTransactionType, andAssetidentify the limit (Section 3.5.1). If it doesn't exist, it is created. If it exists, its restrictions are updated.Bob + Payment + XRPdoesn't touchBob + EscrowCreate + XRPor any RLUSD limit.SpendingLimitSettransactions in a Batch. This avoids a window where the first limit is active (so deny-by-default applies) but the others don't exist yet.SpendingLimitSetis not delegable. Only the owner can manage limits (see Section 8).3.6.2. Example JSON
Alice configures Bob's XRP limits for Payment and EscrowCreate with two transactions. They can be submitted together in a Batch so both take effect atomically.
{ "TransactionType": "SpendingLimitSet", "Account": "rAlice...", "Delegate": "rBob...", "LimitedTransactionType": "Payment", "Asset": { "currency": "XRP" }, "MaxAmount": "100000000", "MaxTransactions": 5, "MinInterval": 86400 }{ "TransactionType": "SpendingLimitSet", "Account": "rAlice...", "Delegate": "rBob...", "LimitedTransactionType": "EscrowCreate", "Asset": { "currency": "XRP" }, "MaxAmount": "1000000000" }3.7. Interaction with Permission Delegation
Permission Delegation stays responsible for granting and revoking permissions. Limits are managed independently, and
DelegateSetand theDelegateledger entry are unchanged.SpendingLimitentries stay until Alice removes them withSpendingLimitSetand recovers her reserve. This avoids deleting potentially thousands of entries in one revocation.3.8. Full Example
Alice has two AI agents, Bob and Charlie.
Bob + Payment + XRP. He has 4 payments left. His EscrowCreate and RLUSD limits are unchanged.Bob + EscrowCreate + XRPand allowed.4. Rationale
4.1. Why limits per transaction type?
Agents need to do more than send payments, and an owner will want different limits for different operations. For example, Alice might allow Bob to pay 100 XRP but lock 1,000 XRP in escrow. A single asset-wide limit can't express that.
4.2. Why deny by default?
Per-transaction-type limits on their own leave unconfigured transaction types unrestricted, which is the bypass described in Section 3.3. Denying unconfigured types once limits exist means:
4.3. Why only the submitter's limits?
The trade-off is that there is no single combined cap: an owner's total exposure is the sum of what all their limits allow, across all scopes and transaction types. An optional account-wide cap could be added later without changing this model.
4.4. Why separate ledger entries instead of an array?
Storing limits as an array on
AccountRootorDelegatewould mean scanning a potentially large array on every transaction and rewriting it on every update. A separate entry per limit allows:The design follows patterns from AccountSet, DepositPreauth, and TrustSet.
4.5. Why a separate transaction from
DelegateSet?4.6. Alternatives considered
4.7. Related work: XLS-86 (Firewall)
XLS-86 (Firewall) by
@dangell7overlaps with this proposal. Firewall's handling of multiple transaction types to prevent bypass is relevant to Section 3.3.Per-delegate, per-asset limits matter when an owner runs several agents with different responsibilities. One agent may only need to send 100 RLUSD per transaction, while another needs 10,000.
5. Backwards Compatibility
This is a new amendment. It adds a new transaction type and a new ledger entry type.
DelegateSetand theDelegateledger entry are unchanged.6. Test Plan
Planned test coverage, in outline:
SpendingLimitSet: create, modify, and remove; account-level and delegate-specific; reserve; non-existent delegate; not delegable; several limits configured atomically in a Batch.7. Reference Implementation
Not yet available. Early work on the transaction and ledger entry definitions has started.
8. Security Considerations
8.1. What limits protect against
SpendingLimitSetnever being delegable. If it were, a delegate could remove its own limits.SpendingLimitSetfor the account can also loosen or remove its account-level limits. They're intended as a safety net, not a substitute for key security or multisigning. (Open: whether loosening or removing a limit should require more than creating one. See Q9.)SetRegularKey,SignerListSet, orAccountDelete. Permission Delegation already prevents delegating these.8.2. Bypass
OfferCreatethat rests on the order book is filled by another trader, a check is cashed by its recipient, and an NFT buy offer is accepted by the NFT's owner. Limits only see the submitter's transaction, so the proposed approach is to check them once, when the commitment is created, against the full committed amount. The later transaction that moves the funds isn't checked. A commitment that is never filled still counts toward the limit, because limits restrict what the submitter does, not the eventual fund movements. (Open, see Q3.)9. Open Questions
AccountRoot(account-level) and theDelegateentry (delegate-specific)?SpendingLimitSet?Appendix
Appendix A: FAQ
A.1: If I limit Bob's XRP payments, can Bob still trade XRP on the DEX?
No, unless you also configure an OfferCreate limit for Bob and XRP. Once Bob has any limit, every asset-moving transaction type needs its own limit for each asset it spends.
A.2: If I never create any limits, does anything change?
No. Transactions behave exactly as they do today.
A.3: Do my account-level limits also apply to my delegates?
No. Only the limits of whoever submits the transaction apply. A delegate with no limits at all is unrestricted, subject to its permissions.
A.4: What happens to limits when I revoke a delegate's permission?
They stay in the ledger and are no longer used, because the delegate can't submit the transaction. You can remove them with
SpendingLimitSetto recover your reserve.A.5: Can a delegate change its own limits?
No.
SpendingLimitSetcan only be submitted by the owner and can't be delegated.A.6: Why not just enforce limits in the agent's software?
The agent's software is what we're guarding against. An agent that hallucinates or misunderstands an instruction can also bypass its own checks. Protocol-level limits apply no matter what the agent does.
All reactions