Start accepting recurring payments with Base Pay Subscriptions
Base Subscriptions enable you to build predictable, recurring revenue streams by accepting automatic USDC payments. Whether you’re running a SaaS platform, content subscription service, or any business model requiring regular payments, Base Subscriptions provide a seamless solution with no merchant fees.Key Capabilities:
Flexible Billing Periods
Support any billing cycle that fits your business model:
Daily subscriptions for short-term services
Weekly for regular deliveries or services
Monthly for standard SaaS subscriptions
Annual for discounted long-term commitments
Custom periods (e.g., 14 days, 90 days) for unique models
Partial and Usage-Based Charging
Charge any amount up to the permitted limit:
Fixed recurring amounts for predictable billing
Variable usage-based charges within a cap
Tiered pricing with different charge amounts
Prorated charges for mid-cycle changes
Subscription Management
Full control over the subscription lifecycle:
Real-time status checking to verify active subscriptions
Base Subscriptions leverage Spend Permissions – a powerful onchain primitive that allows users to grant revocable spending rights to applications. Here’s the complete flow:
1
User Approves Subscription
Your customer grants your application permission to charge their wallet up to a specified amount each billing period. This is a one-time approval that remains active until cancelled.
2
Application Charges Periodically
Your backend service charges the subscription when payment is due, without requiring any user interaction. You can charge up to the approved amount per period.
3
Smart Period Management
The spending limit automatically resets at the start of each new period. If you don’t charge the full amount in one period, it doesn’t roll over.
4
User Maintains Control
Customers can view and cancel their subscriptions anytime through their wallet, ensuring transparency and trust.
A complete subscription implementation requires both client and server components:Client-Side (Frontend):
User interface for subscription creation
Create wallet requests and handle user responses
Server-Side (Backend - Node.js):
CDP smart wallet for executing charges and revocations
Scheduled jobs for periodic billing
Database for subscription tracking
Handlers for status updates
Retry logic for failed charges
CDP-Powered BackendBase Subscriptions use CDP (Coinbase Developer Platform) server wallets for effortless backend management. The charge() and revoke() functions handle all transaction details automatically:
✅ Automatic wallet management
✅ Built-in transaction signing
✅ Gas estimation and nonce handling
✅ Optional paymaster support for gasless transactions
First, set up your CDP smart wallet that will act as the subscription owner:
backend/setup.ts
Report incorrect code
Copy
Ask AI
import { base } from '@base-org/account/node';// Backend setup (Node.js only)// Set CDP credentials as environment variables:// CDP_API_KEY_ID, CDP_API_KEY_SECRET, CDP_WALLET_SECRET// PAYMASTER_URL (recommended for gasless transactions)async function setupSubscriptionWallet() { try { // Create or retrieve your subscription owner wallet (CDP smart wallet) const wallet = await base.subscription.getOrCreateSubscriptionOwnerWallet({ walletName: 'my-app-subscriptions' // Optional: customize wallet name }); console.log('✅ Subscription owner wallet ready!'); console.log(`Smart Wallet Address: ${wallet.address}`); console.log(`Wallet Name: ${wallet.walletName}`); // Make this address available to your frontend // Option 1: Store in database/config // Option 2: Expose via API endpoint // Option 3: Set as public environment variable (e.g., NEXT_PUBLIC_SUBSCRIPTION_OWNER) return wallet; } catch (error) { console.error('Failed to setup wallet:', error.message); throw error; }}// Run once at application startupsetupSubscriptionWallet();// Optional: Provide an API endpoint for the frontend to fetch the addressexport async function getSubscriptionOwnerAddress() { const wallet = await base.subscription.getOrCreateSubscriptionOwnerWallet(); return wallet.address;}
Backend Only: This setup runs in your Node.js backend with CDP credentials. The resulting wallet address is public and safe to share with your frontend for use in subscribe() calls.
Keep CDP Credentials Private: Never expose CDP credentials (API key, secrets) to the frontend. Only the subscription owner wallet address needs to be accessible to the frontend.
Cancel subscriptions programmatically from your backend:
revokeSubscription.ts
Report incorrect code
Copy
Ask AI
import { base } from '@base-org/account/node';async function revokeSubscription(subscriptionId: string, reason: string) { try { // Revoke the subscription with paymaster for gasless transactions const result = await base.subscription.revoke({ id: subscriptionId, paymasterUrl: process.env.PAYMASTER_URL, // Optional: for gasless transactions testnet: false }); console.log(`✅ Revoked subscription: ${subscriptionId}`); console.log(`Transaction: ${result.id}`); console.log(`Reason: ${reason}`); return { success: true, transactionHash: result.id }; } catch (error) { console.error('Revoke failed:', error); return { success: false, error: error.message }; }}// Usage examplesasync function handleUserCancellation(subscriptionId: string) { return await revokeSubscription(subscriptionId, 'user_requested');}async function handlePolicyViolation(subscriptionId: string) { return await revokeSubscription(subscriptionId, 'policy_violation');}
Automatic Transaction Management: The charge() and revoke() functions handle all transaction details including wallet management, gas estimation, nonce handling, and transaction confirmation. Use the paymasterUrl parameter to enable gasless transactions for your users.
Gasless Transactions: Set the PAYMASTER_URL environment variable to sponsor gas fees for your subscription charges and revocations. This creates a seamless experience where your backend covers all gas costs. Get your paymaster URL from the CDP Portal.
By default, charged USDC remains in your subscription owner wallet. You can optionally specify a recipient address to automatically transfer funds to a different address:
Default (Keep in Owner Wallet)
Send to Treasury Wallet
Dynamic Recipients
Report incorrect code
Copy
Ask AI
// Funds stay in the subscription owner walletconst result = await base.subscription.charge({ id: subscriptionId, amount: 'max-remaining-charge', testnet: false});// USDC is now in your CDP smart wallet// Access it later or transfer as needed
Report incorrect code
Copy
Ask AI
// Automatically send to your treasury walletconst result = await base.subscription.charge({ id: subscriptionId, amount: 'max-remaining-charge', recipient: '0xYourTreasuryAddress', testnet: false});// USDC is sent directly to the recipient addressconsole.log(`Sent ${result.amount} to ${result.recipient}`);
Report incorrect code
Copy
Ask AI
// Send to different addresses based on subscription typeasync function chargeWithRecipient(subscriptionId: string, plan: string) { const recipients = { premium: '0xPremiumTreasuryAddress', basic: '0xBasicTreasuryAddress', enterprise: '0xEnterpriseTreasuryAddress' }; return await base.subscription.charge({ id: subscriptionId, amount: 'max-remaining-charge', recipient: recipients[plan], testnet: false });}
Custom Implementations Possible: While Base Subscriptions are optimized for USDC on Base, you can use the underlying Spend Permissions primitive to build custom subscription implementations with any ERC-20 token or native ETH on any EVM-compatible chain.
For developers who need manual control over transaction execution or want to integrate with existing wallet infrastructure, use the lower-level utilities:
prepareCharge - Manual Charge Execution
If you can’t use CDP wallets, prepareCharge() gives you call data to execute manually:
Report incorrect code
Copy
Ask AI
import { base } from '@base-org/account';// Prepare charge call dataconst chargeCalls = await base.subscription.prepareCharge({ id: subscriptionId, amount: 'max-remaining-charge', testnet: false});// Execute with your own wallet infrastructure// (requires custom wallet client setup)
import { base } from '@base-org/account';// Prepare revoke call dataconst revokeCall = await base.subscription.prepareRevoke({ id: subscriptionId, testnet: false});// Execute with your own wallet infrastructure