Tally Protocol
A Solana-native subscription platform enabling merchants to collect recurring USDC payments through SPL Token delegate approvals. Tally implements delegate-based recurring payments, eliminating the need for user signatures on each renewal while maintaining full user control.
Overview
Tally Protocol provides a decentralized subscription management system on Solana where:
- Merchants create subscription plans with flexible pricing and billing periods
- Subscribers approve multi-period USDC allowances through token delegates
- Keepers execute renewals permissionlessly via delegate transfers
- Platform earns fees while providing infrastructure and emergency controls
The protocol uses a single-delegate architecture where subscribers approve a merchant-specific delegate PDA for automatic payment collection, enabling seamless recurring billing without repeated user interactions.
Key Features
Merchant Capabilities
- Register with USDC treasury and configurable fee rates
- Create unlimited subscription plans with custom pricing and periods
- Update plan terms (price, period, grace period, name) without creating new plans
- Earn tiered revenue based on merchant tier (Free: 98%, Pro: 98.5%, Enterprise: 99%)
- Control plan availability and subscriber management
Subscriber Experience
- Start subscriptions with single delegate approval
- Cancel subscriptions anytime and revoke delegate access
- Close canceled subscriptions to reclaim rent (~0.00099792 SOL)
- Benefit from grace periods on failed payments
- Maintain complete control over token approvals
Platform Features
- Tiered merchant fee structure (2.0% / 1.5% / 1.0%)
- Configurable keeper incentives (0.5% renewal fee)
- Emergency pause mechanism for platform protection
- Two-step authority transfer for platform governance
- Fee withdrawal and treasury management
Technical Architecture
- Built with Anchor 0.31.1 on Solana 3.0
- Supports both SPL Token and Token-2022 programs
- Forbids unsafe code with comprehensive clippy lints
- Implements checked arithmetic and explicit access controls
- Emits detailed events for off-chain indexing
Project Structure
tally-protocol/
├── program/ # Anchor program (Solana smart contract)
│ └── src/
│ ├── lib.rs # Program entry point
│ ├── state.rs # Account structures
│ ├── errors.rs # Custom error types
│ ├── events.rs # Event definitions
│ ├── constants.rs # Protocol constants
│ ├── start_subscription.rs # Start new subscription
│ ├── renew_subscription.rs # Renew existing subscription
│ ├── cancel_subscription.rs # Cancel subscription
│ ├── close_subscription.rs # Close canceled subscription
│ ├── create_plan.rs # Create subscription plan
│ ├── update_plan.rs # Update plan status
│ ├── update_plan_terms.rs # Update plan pricing/terms
│ ├── init_merchant.rs # Initialize merchant
│ ├── update_merchant_tier.rs # Update merchant tier
│ ├── init_config.rs # Initialize global config
│ ├── update_config.rs # Update global config
│ ├── admin_withdraw_fees.rs # Withdraw platform fees
│ ├── transfer_authority.rs # Initiate authority transfer
│ ├── accept_authority.rs # Accept authority transfer
│ ├── cancel_authority_transfer.rs # Cancel authority transfer
│ ├── pause.rs # Emergency pause
│ ├── unpause.rs # Disable pause
│ └── utils.rs # Shared utilities
│
├── sdk/ # Rust SDK for program interaction
│ └── src/
│ ├── lib.rs # SDK entry point
│ ├── client.rs # Client for program calls
│ ├── accounts.rs # Account fetching utilities
│ ├── transactions.rs # Transaction builders
│ ├── events.rs # Event parsing
│ └── utils.rs # Helper functions
│
├── packages/ # TypeScript/JavaScript packages
│ ├── idl/ # Program IDL definitions
│ ├── sdk/ # TypeScript SDK
│ └── types/ # Shared type definitions
│
├── examples/ # Usage examples
│ ├── subscribe/ # Subscribe to a plan
│ ├── cancel/ # Cancel a subscription
│ └── list-plans/ # List available plans
│
└── docs/ # Documentation
├── SUBSCRIPTION_LIFECYCLE.md # Lifecycle management guide
├── MULTI_MERCHANT_LIMITATION.md # Single-delegate constraints
├── SPAM_DETECTION.md # Spam prevention strategies
├── RATE_LIMITING_STRATEGY.md # Rate limiting implementation
└── OPERATIONAL_PROCEDURES.md # Platform operations guide
Account Structure
Config (138 bytes)
Global program configuration managed by platform authority.
Fields:
platform_authority- Platform admin with governance rightspending_authority- Two-step authority transfer stagingplatform_treasury- USDC destination for platform feesusdc_mint- USDC token mint addresskeeper_fee_bps- Keeper incentive (basis points, max 100)min_platform_fee_bps- Minimum merchant tier fee (basis points)max_platform_fee_bps- Maximum merchant tier fee (basis points)max_grace_period_secs- Maximum subscription grace periodmin_period_secs- Minimum billing period lengthis_paused- Emergency pause statusbump- PDA derivation seed
PDA Derivation: ["config", program_id]
Merchant (108 bytes)
Merchant-specific configuration and treasury.
Fields:
authority- Merchant admin (manages plans and settings)treasury- USDC ATA receiving merchant revenueplatform_fee_bps- Platform fee rate (tier-based)bump- PDA derivation seed
PDA Derivation: ["merchant", authority.key(), program_id]
Merchant Tiers:
- Free: 200 bps (2.0% platform fee, 98% merchant revenue)
- Pro: 150 bps (1.5% platform fee, 98.5% merchant revenue)
- Enterprise: 100 bps (1.0% platform fee, 99% merchant revenue)
Plan (129 bytes)
Subscription plan with pricing and billing configuration.
Fields:
merchant- Merchant pubkey (plan owner)plan_id- Merchant-defined identifiername- Human-readable plan nameprice_usdc- Subscription price (USDC smallest units)period_secs- Billing period length (seconds)grace_period_secs- Payment failure grace periodactive- Plan accepts new subscriptionscreated_ts- Plan creation timestampbump- PDA derivation seed
PDA Derivation: ["plan", merchant.key(), plan_id.as_bytes(), program_id]
Subscription (120 bytes)
Individual user subscription state.
Fields:
plan- Plan pubkeysubscriber- User pubkey (owns subscription)subscriber_usdc_account- User's USDC token accountactive- Subscription status (active/canceled)renewals- Lifetime renewal count (preserved across reactivations)created_ts- Original subscription creation timestampnext_renewal_ts- Next scheduled renewallast_renewed_ts- Last successful renewal timestamplast_amount- Last payment amountin_trial- Trial period statusbump- PDA derivation seed
PDA Derivation: ["subscription", plan.key(), subscriber.key(), program_id]
Note: The renewals counter tracks lifetime renewals across all sessions, not just the current active session. This design maintains complete historical records for loyalty programs and analytics. See Subscription Lifecycle for details.
Payment Flow
Initial Subscription
- User calls
start_subscriptionwith USDC delegate approval - Program validates plan status and user balance
- First payment transfers USDC (deducting keeper fee on renewals only)
- Subscription account created with
active = true - Delegate approval remains for automatic renewals
SubscribedorSubscriptionReactivatedevent emitted
Renewals
- Keeper calls
renew_subscriptionwhencurrent_time >= next_renewal_ts - Program validates subscription status and delegate approval
- Payment transfers via delegate: User USDC → Keeper fee → Platform fee → Merchant treasury
- Subscription updated:
renewals++,next_renewal_ts += period_secs Renewedevent emitted with payment details
Cancellation
- User calls
cancel_subscriptionto stop renewals - Delegate approval revoked on USDC account
- Subscription marked
active = false Canceledevent emitted
Account Closure
- User calls
close_subscriptionon canceled subscription - Subscription account closed and rent reclaimed (~0.00099792 SOL)
SubscriptionClosedevent emitted
Fee Distribution
Each renewal payment is split sequentially:
- Keeper Fee: 0.5% (configurable, max 1%) to renewal executor
- Platform Fee: 1-2% (tier-based) to platform treasury
- Merchant Revenue: Remainder (98-99%) to merchant treasury
Example (100 USDC renewal, Pro merchant):
- Keeper: 0.50 USDC (0.5%)
- Platform: 1.50 USDC (1.5%)
- Merchant: 98.00 USDC (98%)
Program Instructions
Merchant Operations
init_merchant- Initialize merchant account with treasury and fee configurationcreate_plan- Create new subscription plan with pricing and billing termsupdate_plan- Toggle plan active status (does not affect existing subscriptions)update_plan_terms- Update plan price, period, grace period, or nameupdate_merchant_tier- Change merchant tier and platform fee rate
Subscriber Operations
start_subscription- Start new subscription or reactivate canceled subscriptionrenew_subscription- Execute renewal payment via delegate (permissionless)cancel_subscription- Cancel subscription and revoke delegate approvalclose_subscription- Close canceled subscription and reclaim rent
Platform Operations
init_config- Initialize global program configuration (one-time)update_config- Update global parameters (keeper fee, rate limits, fee bounds)admin_withdraw_fees- Withdraw accumulated platform feestransfer_authority- Initiate two-step platform authority transferaccept_authority- Complete authority transfer as pending authoritycancel_authority_transfer- Cancel pending authority transferpause- Enable emergency pause (disables user operations)unpause- Disable emergency pause (re-enables user operations)
Events
The program emits detailed events for off-chain indexing and analytics:
ConfigInitialized- Global configuration createdConfigUpdated- Configuration parameters changedMerchantInitialized- New merchant registeredMerchantTierUpdated- Merchant tier changedPlanCreated- New subscription plan createdPlanUpdated- Plan status changedPlanTermsUpdated- Plan terms modifiedSubscribed- New subscription startedSubscriptionReactivated- Canceled subscription reactivatedRenewed- Subscription renewed successfullyCanceled- Subscription canceledSubscriptionClosed- Subscription account closedFeesWithdrawn- Platform fees withdrawnAuthorityTransferInitiated- Authority transfer proposedAuthorityTransferAccepted- Authority transfer completedAuthorityTransferCanceled- Authority transfer canceledPaused- Emergency pause enabledUnpaused- Emergency pause disabledDelegateMismatchWarning- Renewal failed due to delegate mismatch
Development
Prerequisites
- Rust 1.70+
- Solana CLI 3.0+
- Anchor CLI 0.31.1+
- Node.js 18+ (for TypeScript SDK)
- pnpm (for package management)
Build Program
# Build the Anchor program
# Run program tests
# Run Rust tests with nextest
Build SDK
# Build Rust SDK
# Build TypeScript SDK
Deployment
Devnet
# Build program
# Deploy to devnet
# Program ID: 6jsdZp5TovWbPGuXcKvnNaBZr1EBYwVTWXW1RhGa2JM5
Localnet
# Start local validator
# Deploy to localnet
# Program ID: Fwrs8tRRtw8HwmQZFS3XRRVcKBQhe1nuZ5heB4FgySXV
Testing
# Run all tests
# Run specific test file
# Run Rust unit tests with nextest (faster, better output)
# Run with code coverage
Security
Audit Status
The program has undergone a comprehensive security audit. See SECURITY_AUDIT_REPORT.md for complete findings and resolutions.
Key Findings:
- Medium (1): SPL Token single-delegate limitation (architectural constraint, documented)
- Low (3): All resolved through code improvements and documentation
- Informational (4): All addressed with enhanced documentation and operational procedures
Security Features
#![forbid(unsafe_code)]- No unsafe Rust code allowed- Comprehensive clippy lints (
arithmetic_side_effects,default_trait_access) - Checked arithmetic operations preventing overflow/underflow
- Explicit access control on all privileged instructions
- Two-step authority transfer preventing accidental ownership loss
- Emergency pause mechanism for platform protection
- Detailed event logging for transparency and auditability
Known Limitations
Single-Delegate Constraint (M-1)
SPL Token accounts support only one delegate at a time. Subscribing to multiple merchants using the same USDC account will overwrite previous delegate approvals, breaking existing subscriptions.
Recommended Mitigation:
- Use separate USDC token accounts for each merchant subscription
- Frontend UI should detect and warn about existing delegates
- Monitor
DelegateMismatchWarningevents for renewal failures
See MULTI_MERCHANT_LIMITATION.md for comprehensive details and integration guidance.
Documentation
- Subscription Lifecycle - Complete lifecycle management guide
- Multi-Merchant Limitation - Single-delegate constraint details
- Spam Detection - Spam prevention strategies
- Rate Limiting Strategy - Rate limiting implementation
- Operational Procedures - Platform operations guide
- Security Audit Report - Comprehensive security audit
Examples
Examples demonstrate common usage patterns (implementations coming soon):
- Subscribe - Start a subscription
- Cancel - Cancel an active subscription
- List Plans - Query available plans
SDK Usage
Rust SDK
use ;
use Signer;
// Initialize client
let client = new?;
// Start a subscription
let subscription_pubkey = client.start_subscription.await?;
// Cancel a subscription
client.cancel_subscription.await?;
// Renew a subscription (keeper)
client.renew_subscription.await?;
TypeScript SDK
import { TallyClient } from '@tally-protocol/sdk';
import { Connection, Keypair } from '@solana/web3.js';
// Initialize client
const connection = new Connection('https://api.devnet.solana.com');
const client = new TallyClient(connection, wallet);
// Start a subscription
const subscriptionPubkey = await client.startSubscription({
plan: planPubkey,
subscriberUsdcAccount: usdcAccount,
delegate: delegatePubkey,
approveAmount: amount,
});
// Cancel a subscription
await client.cancelSubscription(subscriptionPubkey);
// Renew a subscription (keeper)
await client.renewSubscription(subscriptionPubkey);
Contributing
Contributions are welcome! Please follow these guidelines:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit changes with conventional commits (
git commit -S -m 'feat: add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
Development Standards
- All Rust code must pass
cargo clippywith zero warnings - All tests must pass via
cargo nextest run - Unsafe code is forbidden (
#![forbid(unsafe_code)]) - Follow existing code style and documentation patterns
- Sign all commits (
git commit -S)
License
MIT License - see LICENSE file for details
Support
- GitHub Issues: https://github.com/Tally-Pay/tally-protocol/issues
- Documentation: https://github.com/Tally-Pay/tally-protocol/tree/main/docs
- Security: Report vulnerabilities via GitHub Security Advisories
Acknowledgments
Built with: