Appearance
RFC-0001: Mandate Interface for Delegated Pull Payments
Status: Draft
Date: 2026-05-13
Reference implementation: Vela Protocol
Audience: Solana payment protocols, wallet teams, Token-2022 builders, keeper networks, merchants, and subscription products
Summary
This RFC proposes a neutral mandate interface for delegated pull payments on Solana. A mandate is a scoped, revocable authorization that lets an approved payee or executor collect funds from a payer only inside explicit boundaries: asset, amount, timing, destination, expiry, and revocation state.
The goal is interoperability, not control. Different products should be able to build subscriptions, milestones, usage billing, streams, invoices, agent budgets, or private billing on top of their own programs while sharing a common security contract for "is this pull authorized?"
Vela Protocol is used here as the reference implementation because it already implements the core pattern:
- A persistent
VelaMandatePDA stores the payer, merchant, plan, amount, cadence, expiry, status, and execution counters. - A short-lived
PullApprovalPDA authorizes one bounded billing period. execute_pullchecks mandate state before settlement.- A dedicated Token-2022 transfer-hook program re-validates the approval at the exact point where value moves.
- Arcium can privately validate billing conditions before a
PullApprovalis written, but encrypted compute is an optional validation backend rather than a requirement of this interface.
Motivation
Recurring and delegated payments are becoming a shared primitive, but the ecosystem is at risk of fragmenting into isolated payment stacks. If every team defines its own authority model, wallets and merchants must integrate each protocol separately, and one program can accidentally become a gatekeeper for everyone else.
This RFC separates the stable security boundary from product-specific billing logic.
Products should compete on UX, pricing models, privacy, analytics, merchant tools, and distribution. They should not have to compete on incompatible definitions of customer-authorized pull authority.
Goals
- Define the minimum shared account and validation concepts for a delegated payment mandate.
- Preserve the core safety property: no transfer can exceed the customer's approved scope.
- Support multiple implementation programs without forcing a single subscription stack.
- Allow public, committed, or encrypted billing terms.
- Allow adapters and settlement hooks to validate mandates issued by more than one product.
- Make wallet and merchant UX safer by giving them a consistent vocabulary for scoped payment authority.
Non-goals
- This RFC does not define one canonical subscription product.
- This RFC does not require Arcium, C-SPL, confidential transfers, or any specific privacy backend.
- This RFC does not standardize merchant dashboards, pricing pages, checkout UX, or webhook payloads.
- This RFC does not require a shared upgrade authority or governance model.
- This RFC does not solve settlement-layer privacy by itself. If the settlement rail exposes transfer amount, addresses, or timing, those facts remain public even if billing validation is private.
Terminology
| Term | Meaning |
|---|---|
| Payer | The customer, subscriber, wallet, account, or authority granting payment scope. |
| Payee | The merchant, service, protocol, or account allowed to receive funds. |
| Mandate | A scoped, revocable authorization for future pulls. |
| Pull | A single attempted payment under a mandate. |
| Pull executor | The signer or keeper that submits a pull transaction. This does not need to be the payee. |
| Validator | The program, adapter, hook, or private-compute callback that decides whether a pull is inside scope. |
| Pull authorization | A short-lived approval for one pull or one billing period. Vela calls this PullApproval. |
| Settlement rail | The token movement path. In Vela today this is wrapped USDC on Token-2022 with a transfer hook. |
| Product program | A product-specific program such as Vela, Tributary, or another billing implementation. |
| Adapter program | A neutral program that can validate or translate mandates from multiple product programs. |
Core Invariant
A compliant implementation MUST enforce this invariant:
A pull succeeds only if the payer previously approved a mandate whose current scope authorizes that exact transfer.
That means a pull MUST be bound to:
- the mandate identity;
- the payer or source authority;
- the payee or destination authority;
- the settlement asset or asset registry entry;
- the amount or maximum amount;
- the billing period, cadence, or allowed execution window;
- the expiry and current lifecycle status;
- the revocation state;
- a replay-resistant pull authorization.
An implementation MAY add stricter checks, but it MUST NOT allow an adapter, merchant, keeper, or validator to widen the customer-approved scope without fresh payer consent.
Interface Levels
The interface is intentionally layered so builders can adopt it without replacing their whole stack.
Level 1: Read-compatible mandates
An implementation exposes enough mandate state, metadata, or attestations for wallets, explorers, and downstream apps to answer:
- Who granted authority?
- Who may receive funds?
- Which asset can move?
- What is the maximum scope?
- When does the authorization end?
- Can the payer revoke it?
- Is it active, cancelled, paused, expired, or revoked?
Terms MAY be public, hashed, committed, or encrypted. If terms are encrypted, the implementation SHOULD expose a wallet-readable approval summary signed by the payer and bound to a terms hash.
Level 2: Authorization-compatible pulls
An implementation can produce or verify a standardized pull authorization. The authorization is short-lived and bound to one mandate and one execution window.
The settlement path, transfer hook, or adapter can consume that authorization without understanding the product's full billing model.
Level 3: Adapter-compatible settlement
A neutral adapter can validate product-specific mandates and emit a common settlement authorization. This lets multiple products share the same payment-authority layer while keeping their own plan logic, UI, privacy backend, and merchant tools.
At this level, the adapter MUST remain a verifier and translator. It SHOULD NOT become the only place where new billing products must be built.
Mandate Data Model
The following fields describe the abstract mandate shape. A compliant implementation can store these directly, derive them from product state, or prove them through a commitment or private-compute result.
typescript
type MandateStatus = 'active' | 'paused' | 'cancelled' | 'expired' | 'revoked';
type MandateTermsModel =
| 'fixed_amount'
| 'usage_capped'
| 'stream_rate'
| 'milestone'
| 'agent_budget'
| 'custom';
interface MandateCore {
mandateId: string;
issuerProgram: string;
payer: string;
payee: string;
settlementAsset: string;
sourceAuthority?: string;
destination?: string;
termsModel: MandateTermsModel;
maxAmountPerPull?: string;
maxAmountPerPeriod?: string;
minIntervalSeconds?: number;
validFrom: number;
validUntil?: number;
maxPulls?: number;
pullsExecuted: number;
nextEligibleAt?: number;
status: MandateStatus;
revocationAuthority: string;
version: number;
termsHash?: string;
privacyMode?: 'public' | 'committed' | 'encrypted';
}Mapping to Vela Protocol
| Interface field | Vela reference field |
|---|---|
mandateId | VelaMandate PDA |
issuerProgram | vela_protocol program id |
payer | VelaMandate.subscriber |
payee | VelaMandate.merchant |
settlementAsset | TokenConfig.mint / plan billing mint |
termsModel | VelaMandate.billing_type plus plan type |
maxAmountPerPull | VelaMandate.amount for fixed billing; usage cap for usage billing |
minIntervalSeconds | VelaMandate.frequency |
validFrom | VelaMandate.start_date |
validUntil | VelaMandate.expiry |
maxPulls | VelaMandate.max_pulls |
pullsExecuted | VelaMandate.pulls_executed |
nextEligibleAt | VelaMandate.next_payment_due |
status | VelaMandate.status |
version | VelaMandate.version |
Pull Authorization Data Model
A pull authorization is not the same as a mandate. The mandate is the durable customer grant. The pull authorization is a short-lived proof that a specific pull is currently allowed.
typescript
interface PullAuthorization {
authorizationId: string;
mandateId: string;
payer: string;
payee: string;
settlementAsset: string;
approvedAmount: string;
periodStart: number;
periodEnd: number;
validUntil: number;
destination?: string;
nonce?: string;
approved: boolean;
issuedBy: string;
proofType: 'program' | 'adapter' | 'signature' | 'zk' | 'mpc';
proof?: string;
}Mapping to Vela Protocol
| Interface field | Vela reference field |
|---|---|
authorizationId | PullApproval PDA |
mandateId | PullApproval.mandate |
approvedAmount | PullApproval.approved_amount |
periodStart | PullApproval.period_start |
periodEnd | PullApproval.period_end |
validUntil | PullApproval.valid_until |
approved | PullApproval.approved |
issuedBy | validate_mandate_callback, usage_charge_callback, or equivalent validator |
proofType | mpc for Arcium-backed approvals, program for direct public validation |
Required Operations
The interface does not require exact instruction names. It requires equivalent behavior.
Create mandate
Creates a payer-authorized mandate.
Requirements:
- The payer MUST sign, or the payer's authorized account MUST sign.
- The payer MUST be shown the effective scope before approval.
- The mandate MUST be bound to one settlement asset or registered asset policy.
- The mandate MUST have an explicit revocation path.
- The mandate MUST initialize as active or pending, never silently active with hidden terms.
Vela reference: subscribe.
Revoke mandate
Revokes future pulls.
Requirements:
- The payer or revocation authority MUST be able to revoke.
- After revocation, new pull authorizations MUST fail.
- Existing unconsumed pull authorizations SHOULD be invalidated or made unusable by status checks.
Vela reference: cancel, with admin_cancel as an emergency override path.
Request authorization
Asks a validator to evaluate whether a pull is currently allowed.
Requirements:
- The request MUST identify the mandate and period.
- The request MUST be replay-resistant.
- The validator MUST fail closed if required data is missing, stale, invalid, or unavailable.
Vela reference: request_validation and request_usage_computation.
Issue pull authorization
Writes or returns the short-lived authorization consumed by settlement.
Requirements:
- The authorization MUST be bound to one mandate.
- The authorization MUST include an amount ceiling.
- The authorization MUST include an expiry.
- The authorization MUST include a period or nonce binding.
- The authorization MUST be impossible to reuse outside its scope.
Vela reference: validate_mandate_callback, usage_charge_callback, and PullApproval.
Execute pull
Attempts settlement.
Requirements:
- The pull MUST check mandate status, expiry, max pulls, timing, payer, payee, and settlement asset.
- The pull MUST check or consume a valid pull authorization.
- The transfer path MUST re-check the authorization at settlement time when the rail supports hooks or native validation.
- A successful pull MUST update execution counters before another equivalent pull can succeed.
- A successful pull SHOULD consume or close the pull authorization.
Vela reference: execute_pull plus vela_transfer_hook.
Validation Rules
A compliant validator MUST reject a pull when any of the following are true:
- mandate does not exist;
- mandate is not active;
- payer, payee, source, destination, or asset does not match;
- amount exceeds the approved scope;
- the pull is earlier than the next eligible execution time;
- the mandate has expired;
- the mandate has reached max pulls or max spend;
- an earlier required billing record or settlement record is missing;
- the pull authorization is missing, expired, false, malformed, or for a different period;
- the validator cannot verify the required proof or private-compute output;
- the settlement hook receives an unexpected account set.
Settlement Hook Behavior
For Token-2022 rails, a compliant transfer hook SHOULD validate:
- the owner/source authority is a mandate or approved delegated authority;
- the pull authorization PDA or equivalent account is derived from the mandate;
- the authorization belongs to the mandate;
- the authorization is approved and unexpired;
- the transfer amount is less than or equal to the approved amount;
- the authorization period matches the current mandate billing period when applicable.
Vela's transfer hook follows this pattern by loading the mandate or stream owner, deriving the expected PullApproval, and rejecting transfers without a valid approval.
Adapter Program Model
A neutral adapter can let different billing implementations share a settlement layer.
text
Product mandate program
|
| validate product-specific terms
v
Neutral mandate adapter
|
| emit standard PullAuthorization
v
Settlement rail / transfer hook
|
| move funds only if authorization is valid
v
PayeeAdapter requirements:
- The adapter MUST support multiple registered issuer programs.
- The adapter MUST verify issuer ownership and expected account derivations.
- The adapter MUST NOT let one issuer mutate another issuer's mandates.
- The adapter MUST NOT widen any issuer's mandate scope.
- The adapter MUST expose issuer-specific risk and upgrade metadata so wallets can display who is trusted.
- The adapter SHOULD make registration permissionless or governed by a transparent allowlist.
The adapter can standardize authorization without standardizing products.
Privacy Profiles
Mandates can expose different levels of information.
| Profile | Description | Example |
|---|---|---|
| Public terms | All scope fields are visible on-chain. | Simple subscriptions. |
| Committed terms | Terms are represented by hashes or commitments, with wallet-readable summaries signed by the payer. | Private plan names or private tier details. |
| Encrypted validation | Inputs and billing logic are encrypted; validator outputs only approval and amount ceiling. | Vela's Arcium-backed usage and mandate validation. |
| Confidential settlement | Transfer amount and/or balances are private at the token rail. | Future C-SPL or confidential-transfer integration. |
This RFC only requires that the settlement validator can determine whether a pull is authorized. It does not require all terms to be public.
However, payer consent must stay meaningful. If terms are encrypted, wallets and checkout surfaces SHOULD bind the payer's approval to:
- a human-readable scope summary;
- a canonical terms hash;
- the issuer program;
- the settlement asset;
- revocation instructions.
Events
Implementations SHOULD emit or expose equivalent events:
| Event | Required fields |
|---|---|
MandateCreated | mandate id, issuer program, payer, payee, asset, terms hash or public scope, timestamp |
MandateUpdated | mandate id, changed scope, effective time, payer approval indicator |
MandateRevoked | mandate id, revoker, timestamp |
PullAuthorizationIssued | authorization id, mandate id, amount ceiling, period, valid until |
PullExecuted | mandate id, authorization id, amount, asset, payer/source, payee/destination, timestamp |
PullRejected | mandate id, reason, timestamp |
MandateExpired | mandate id, reason, timestamp |
Wallet Display Requirements
Wallets and checkout surfaces SHOULD display mandates as scoped authority, not as generic token approvals.
At minimum, the payer should be able to see:
- who can receive funds;
- which asset can move;
- the maximum pull amount or maximum period spend;
- the cadence or settlement window;
- the expiry or max-pull limit;
- whether terms are public, committed, or encrypted;
- how to revoke;
- which issuer program and adapter will enforce the mandate.
Compatibility Profiles
Fixed subscription profile
Required fields:
- payer;
- payee;
- asset;
- fixed amount;
- minimum interval or next eligible pull;
- expiry or max pulls;
- active/cancelled/expired state.
This is the base profile implemented by Vela's flat plans.
Usage-capped profile
Required fields:
- all fixed subscription fields;
- maximum charge per period;
- period-bound usage report or usage commitment;
- validator output that computes or approves the charge.
This is implemented by Vela's usage report and Arcium usage computation path.
Streaming profile
Required fields:
- payer;
- payee;
- asset;
- rate per second;
- minimum settlement interval;
- optional maximum streamed amount;
- last settlement timestamp;
- active/paused/cancelled state.
Vela implements this as StreamMandate, with transfer-hook validation for accrued amount and cap.
Agent budget profile
Required fields:
- authority;
- agent;
- asset;
- daily limit;
- lifetime cap;
- minimum pull amount;
- cooldown;
- optional service allowlist;
- active/paused/revoked state.
Vela implements this as AgentMandate.
Security Considerations
Permissionless execution is acceptable only with scoped validation
Anyone MAY submit a pull transaction if the program enforces all constraints. The executor is not trusted. The mandate, validator, and settlement hook are trusted to reject invalid pulls.
Adapters must be monotonic
An adapter may narrow scope, shorten validity, lower limits, or reject a pull. It must not increase amount, extend expiry, change asset, change destination, or bypass revocation without a new payer approval.
Pull authorizations should be short-lived
Long-lived authorizations recreate the risk of broad token approvals. Authorizations should expire quickly and be bound to a billing period or nonce.
Transfer-time validation is preferred
If the settlement rail supports hooks or native authorization checks, validation should happen at transfer time. Preflight validation alone is insufficient because a direct token transfer may bypass application-layer checks.
Upgrade authority must be visible
Wallets and integrators should know which program can change enforcement behavior. Adapter registration should expose the issuer program, adapter program, and upgrade authority or immutability status.
Open Questions
- Should the ecosystem standardize a concrete
PullAuthorizationaccount layout, or only a semantic interface? - Should a neutral adapter be governed, immutable, or registry-based?
- How should wallets display encrypted terms without exposing private billing data?
- How should multiple products share a Token-2022 settlement mint when a mint can point at only one transfer hook?
- Should confidential settlement be a separate RFC once C-SPL or another rail is production-ready?
- Which fields are required for the minimum wallet-safe approval screen?
- Should revocation be required to be direct payer-signed, or can product-specific delegation revoke on the payer's behalf?
Proposed Next Steps
- Review this draft with Tributary and the Solana Foundation subscription-contract team.
- Agree on the minimum Level 1 read-compatible fields.
- Draft a concrete
PullAuthorizationaccount schema and IDL. - Prototype a neutral adapter that can validate Vela mandates first, then add a second issuer implementation.
- Create wallet UX copy for mandate approval and revocation.
- Decide whether shared settlement should use one transfer-hook router, per-product wrapped assets, or a later confidential settlement rail.
Appendix: Vela Implementation Notes
Vela's current implementation is more opinionated than this RFC:
- It uses Token-2022 wrapped USDC as the settlement rail.
- It uses a dual-program design:
vela_protocolfor product logic andvela_transfer_hookfor transfer-time enforcement. - It uses a
PullApprovalPDA as an ephemeral authorization. - It supports Arcium-backed validation for private billing logic.
- It supports fixed subscriptions, usage billing, streams, and agent budgets.
Those choices prove the interface can work, but they are not all required for interoperability. The proposed standard is the narrower contract: customer-approved scope, revocation, authorization freshness, and settlement-time enforcement.