| Crates.io | tally-sdk |
| lib.rs | tally-sdk |
| version | 0.2.1 |
| created_at | 2025-10-04 20:43:37.352279+00 |
| updated_at | 2025-10-06 16:08:05.160658+00 |
| description | Rust SDK for the Tally Solana subscriptions platform |
| homepage | https://github.com/Tally-Pay/tally-protocol |
| repository | https://github.com/Tally-Pay/tally-protocol |
| max_upload_size | |
| id | 1868384 |
| size | 770,577 |
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.
Tally Protocol provides a decentralized subscription management system on Solana where:
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.
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
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 seedPDA Derivation: ["config", program_id]
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 seedPDA Derivation: ["merchant", authority.key(), program_id]
Merchant Tiers:
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 seedPDA Derivation: ["plan", merchant.key(), plan_id.as_bytes(), program_id]
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 seedPDA 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.
start_subscription with USDC delegate approvalactive = trueSubscribed or SubscriptionReactivated event emittedrenew_subscription when current_time >= next_renewal_tsrenewals++, next_renewal_ts += period_secsRenewed event emitted with payment detailscancel_subscription to stop renewalsactive = falseCanceled event emittedclose_subscription on canceled subscriptionSubscriptionClosed event emittedEach renewal payment is split sequentially:
Example (100 USDC renewal, Pro merchant):
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 ratestart_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 rentinit_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)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# Build the Anchor program
anchor build
# Run program tests
anchor test
# Run Rust tests with nextest
cargo nextest run
# Build Rust SDK
cd sdk
cargo build
cargo test
# Build TypeScript SDK
cd packages/sdk
pnpm install
pnpm build
# Build program
anchor build
# Deploy to devnet
anchor deploy --provider.cluster devnet
# Program ID: 6jsdZp5TovWbPGuXcKvnNaBZr1EBYwVTWXW1RhGa2JM5
# Start local validator
solana-test-validator
# Deploy to localnet
anchor deploy --provider.cluster localnet
# Program ID: Fwrs8tRRtw8HwmQZFS3XRRVcKBQhe1nuZ5heB4FgySXV
# Run all tests
anchor test
# Run specific test file
anchor test tests/subscription.ts
# Run Rust unit tests with nextest (faster, better output)
cargo nextest run
# Run with code coverage
cargo llvm-cov nextest
The program has undergone a comprehensive security audit. See SECURITY_AUDIT_REPORT.md for complete findings and resolutions.
Key Findings:
#![forbid(unsafe_code)] - No unsafe Rust code allowedarithmetic_side_effects, default_trait_access)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:
DelegateMismatchWarning events for renewal failuresSee MULTI_MERCHANT_LIMITATION.md for comprehensive details and integration guidance.
Examples demonstrate common usage patterns (implementations coming soon):
use tally_sdk::{TallyClient, accounts::*, transactions::*};
use solana_sdk::signer::Signer;
// Initialize client
let client = TallyClient::new(rpc_url, payer)?;
// Start a subscription
let subscription_pubkey = client.start_subscription(
&plan_pubkey,
&subscriber_usdc_account,
&delegate_pubkey,
approve_amount,
).await?;
// Cancel a subscription
client.cancel_subscription(&subscription_pubkey).await?;
// Renew a subscription (keeper)
client.renew_subscription(&subscription_pubkey).await?;
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);
Contributions are welcome! Please follow these guidelines:
git checkout -b feature/amazing-feature)git commit -S -m 'feat: add amazing feature')git push origin feature/amazing-feature)cargo clippy with zero warningscargo nextest run#![forbid(unsafe_code)])git commit -S)MIT License - see LICENSE file for details
Built with: