xip:
title: Delegated Signing for User-Funded Messages
description: Enable users to pay for their own message sending by authorizing gateways to sign payer envelopes on their behalf
author: @tantodefi
discussions-to: GitHub · Where software is built
status: Draft
type: Standards
category: Core
created: 2026-02-04
Abstract
This XIP introduces delegated signing, a mechanism that allows XMTP users to pay for their own message sending by authorizing gateway operators to sign payer envelopes on their behalf. Users create on-chain delegation authorizations in the PayerRegistry contract, enabling gateways to include delegation information when signing envelopes. The network then charges the user’s payer balance instead of the gateway’s balance.
Motivation
The current XMTP payment architecture has a fundamental limitation:
-
Client constructs a message envelope
-
Gateway wraps it in a
PayerEnvelopesigned withXMTPD_PAYER_PRIVATE_KEY -
Network charges the payer whose key signed the envelope
-
The payer charged is determined by who signs, not who sent the message
This means users cannot pay for their own messages even if they’ve funded an on-chain payer balance, because the gateway always signs with its own key.
This limitation prevents:
-
Users from having direct control over their messaging costs
-
Business models where users pay per-message
-
Decoupling gateway operation costs from message volume
-
True self-sovereign messaging where users manage their own balances
Delegated signing solves this by allowing users to authorize specific gateways to sign on their behalf while charging the user’s balance.
A proof-of-concept demonstrating user-funded messaging with Aave yield has been implemented:
-
Repository: tantodefi/gateway-console
-
Issue: xmtp/xmtpd#1599
Specification
The keywords “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.
Smart Contract Changes
Delegation Struct
A new struct MUST be added to IPayerRegistry:
struct Delegation {
bool isActive;
uint64 expiry; // 0 = no expiry
uint64 createdAt;
}
Events
The following events MUST be emitted:
-
DelegationAuthorized(address indexed payer, address indexed delegate, uint64 expiry)- Emitted when a delegation is created -
DelegationRevoked(address indexed payer, address indexed delegate)- Emitted when a delegation is revoked
Errors
The following errors MUST be defined:
-
ZeroDelegate()- Reverts when delegate address is zero -
DelegationAlreadyExists()- Reverts when creating duplicate delegation -
DelegationDoesNotExist()- Reverts when revoking non-existent delegation -
DelegationExpiryInPast()- Reverts when expiry timestamp is in the past
Functions
The following functions MUST be implemented:
authorize(address delegate, uint64 expiry)
Grants delegation authority to a gateway address.
-
MUST revert with
ZeroDelegate()if delegate is zero address -
MUST revert with
DelegationAlreadyExists()if delegation already exists -
MUST revert with
DelegationExpiryInPast()if expiry is non-zero and in the past -
MUST emit
DelegationAuthorizedevent -
An expiry of 0 MUST indicate no expiration
revoke(address delegate)
Revokes a previously granted delegation.
-
MUST revert with
ZeroDelegate()if delegate is zero address -
MUST revert with
DelegationDoesNotExist()if delegation does not exist -
MUST emit
DelegationRevokedevent
isAuthorized(address payer, address delegate) → bool
Checks if a delegation is currently valid.
-
MUST return
trueonly if delegation is active AND (expiry is 0 OR expiry > block.timestamp) -
MUST return
falseotherwise
getDelegation(address payer, address delegate) → Delegation
Returns the delegation information for a payer/delegate pair.
Protocol Changes
PayerEnvelope Extension
The PayerEnvelope protobuf message MUST include an optional delegation field:
message PayerEnvelope {
bytes unsigned_client_envelope = 1;
Signature payer_signature = 2;
bytes delegated_payer_address = 3; // Optional: 20-byte Ethereum address
}
When delegated_payer_address is set:
-
payer_signatureMUST be from the gateway (delegate) -
Fees MUST be charged to
delegated_payer_address(user)
Gateway Behavior
When a gateway receives a request with delegation:
-
The gateway MUST verify the delegation is valid on-chain (caching is RECOMMENDED)
-
If valid, the gateway MUST include
delegated_payer_addressin thePayerEnvelope -
If invalid, the gateway SHOULD fall back to gateway payment or reject the request
Node Validation
Nodes receiving envelopes with delegated_payer_address:
-
MUST verify the delegation is valid on-chain before accepting
-
MUST charge fees to
delegated_payer_addressinstead of the signer -
MUST reject envelopes with invalid or expired delegations
Rate Limiting
Implementations SHOULD apply dual rate limiting for delegated requests:
-
Gateway limits: Overall throughput cap for the gateway
-
User limits: Per-user message rate limits
Both limits MUST pass for a delegated request to be allowed.
Caching Considerations
Implementations MAY cache delegation status to reduce on-chain queries.
-
RECOMMENDED default TTL: 5 minutes
-
Implementations MUST check on-chain expiry timestamps in addition to cache TTL
-
Users SHOULD be informed of cache TTL when revoking delegations
Rationale
Why On-Chain Delegation?
Alternative approaches considered:
- Signed delegation messages: Users could sign off-chain messages authorizing gateways. This was rejected because:
-
No way to revoke without maintaining state
-
Harder to audit active delegations
-
No expiry enforcement
-
Gateway-specific tokens: Users could mint tokens for specific gateways. This was rejected due to complexity and gas costs.
-
Multi-sig envelopes: Require both user and gateway signatures. This was rejected because it requires protocol-level changes to signature verification.
On-chain delegation was chosen because:
-
Clear audit trail
-
Straightforward revocation
-
Expiry enforcement by the protocol
-
Minimal protocol changes required
Why Time-Based Cache TTL?
Alternative caching strategies considered:
-
Event-based invalidation: Subscribe to contract events. This was rejected for initial implementation due to complexity but RECOMMENDED for future enhancement.
-
No caching: Always query on-chain. This was rejected due to RPC load and latency concerns.
-
Infinite cache with explicit invalidation: This was rejected because revocations would never propagate.
Time-based TTL balances:
-
Reduced RPC calls (5 min cache = ~288 calls/day vs 86,400+ without caching)
-
Acceptable revocation latency for most use cases
-
Simple implementation
Why Dual Rate Limiting?
Without user-specific limits, a user with a large balance could monopolize gateway capacity. Dual limiting ensures:
-
Gateway operators can cap total throughput
-
Individual users cannot abuse the system
-
Fair resource allocation across users
Backward Compatibility
This XIP is fully backward compatible:
-
Existing envelopes without
delegated_payer_addresscontinue to work unchanged -
Gateways that don’t support delegation continue to operate normally
-
Users who don’t create delegations are unaffected
-
Nodes MUST support both delegated and non-delegated envelopes
Test Cases
Contract Tests
-
Authorization: User authorizes gateway,
isAuthorizedreturns true -
Revocation: User revokes,
isAuthorizedreturns false -
Expiry: Delegation with past expiry returns false for
isAuthorized -
No expiry: Delegation with expiry=0 never expires
-
Zero address:
authorize(address(0))reverts withZeroDelegate -
Duplicate:
authorizeon existing delegation reverts withDelegationAlreadyExists
Backend Tests
-
Caching: Verify cache hit returns same result without RPC call
-
Cache expiry: Verify cache miss after TTL triggers RPC call
-
Delegation validation: Verify gateway correctly validates before signing
-
Dual rate limiting: Verify both gateway and user limits are enforced
Reference Implementation
Smart Contracts
Backend (xmtpd)
Files Changed
smart-contracts:
-
src/settlement-chain/interfaces/IPayerRegistry.sol -
src/settlement-chain/PayerRegistry.sol -
test/unit/PayerRegistry.t.sol
xmtpd:
-
pkg/constants/constants.go -
pkg/delegation/delegation.go -
pkg/delegation/chain_verifier.go -
pkg/delegation/delegation_test.go -
pkg/api/payer/service.go -
pkg/envelopes/payer.go -
pkg/ratelimiter/interface.go -
pkg/ratelimiter/dual_limiter.go -
pkg/ratelimiter/dual_limiter_test.go
Security Considerations
Threat Model
Malicious Gateway
A gateway authorized by a user could attempt to drain the user’s balance by sending unauthorized messages.
Mitigation:
-
Users SHOULD only authorize trusted gateways
-
Users SHOULD set reasonable expiry times
-
Users SHOULD monitor their balance
-
Per-user rate limits prevent rapid balance drainage
Revocation Latency
Due to caching, a revoked delegation may remain valid for up to the cache TTL (default 5 minutes).
Mitigation:
-
Users SHOULD wait for cache TTL before assuming revocation is effective
-
High-security deployments SHOULD reduce cache TTL
-
Future implementations SHOULD consider event-based invalidation
Delegation Replay
An attacker could attempt to replay old delegation transactions.
Mitigation:
-
isAuthorizedalways checks current on-chain state -
Expiry timestamps are enforced at the contract level
-
Revocation immediately marks delegation as inactive
Balance Exhaustion
A user’s balance could be exhausted by legitimate but excessive usage.
Mitigation:
-
Per-user rate limits cap message throughput
-
SDKs SHOULD provide balance visibility and low-balance warnings
-
Users SHOULD monitor their balance
Recommendations
-
Expiry times: Set delegation expiry to the minimum necessary duration
-
Gateway trust: Only authorize gateways operated by trusted parties
-
Balance monitoring: Implement alerts for low balance conditions
-
Rate limit tuning: Adjust per-user limits based on expected usage patterns
Copyright
Copyright and related rights waived via CC0.