Defined in the Base Account SDK
Base Subscriptions enable recurring USDC payments. Users grant your application permission to charge their account periodically, eliminating the need for manual approvals on each payment. No fees for merchants or users.

How Subscriptions Work

1

User Creates Subscription

User approves a spend permission for your application to charge a specific amount periodically.
2

Application Charges Periodically

Your backend charges the subscription when payment is due, up to the permitted amount per period.
3

Automatic Period Reset

The spending limit resets automatically at the start of each new period.
4

User Can Cancel Anytime

Users maintain full control and can revoke the permission at any time.

Core Functions

Complete Implementation Example

Client-Side: Create Subscriptions

Users create subscriptions from your frontend application:
SubscriptionButton.tsx
import React, { useState } from 'react';
import { base } from '@base-org/account';

export function SubscriptionButton() {
  const [loading, setLoading] = useState(false);
  const [subscribed, setSubscribed] = useState(false);
  const [subscriptionId, setSubscriptionId] = useState('');
  
  const handleSubscribe = async () => {
    setLoading(true);
    
    try {
      // Create subscription
      const subscription = await base.subscription.subscribe({
        recurringCharge: "29.99",
        subscriptionOwner: "0xYourAppWallet", // Replace with your wallet address
        periodInDays: 30,
        testnet: false
      });
      
      // Store subscription ID for future reference
      setSubscriptionId(subscription.id);
      console.log('Subscription created:', subscription.id);
      console.log('Payer:', subscription.subscriptionPayer);
      console.log('Amount:', subscription.recurringCharge);
      console.log('Period:', subscription.periodInDays, 'days');
      
      // Send subscription ID to your backend
      await saveSubscriptionToBackend(subscription.id, subscription.subscriptionPayer);
      
      setSubscribed(true);
      
    } catch (error) {
      console.error('Subscription failed:', error);
      alert('Failed to create subscription: ' + error.message);
    } finally {
      setLoading(false);
    }
  };
  
  const saveSubscriptionToBackend = async (id: string, payer: string) => {
    // Example API call to store subscription in your database
    const response = await fetch('/api/subscriptions', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ subscriptionId: id, payerAddress: payer })
    });
    
    if (!response.ok) {
      throw new Error('Failed to save subscription');
    }
  };
  
  if (subscribed) {
    return (
      <div className="subscription-status">
        <Check>✅ Subscription active</Check>
        <p>Subscription ID: {subscriptionId.slice(0, 10)}...</p>
      </div>
    );
  }
  
  return (
    <button 
      onClick={handleSubscribe} 
      disabled={loading}
      className="subscribe-button"
    >
      {loading ? 'Processing...' : 'Subscribe - $29.99/month'}
    </button>
  );
}

Server-Side: Charge Subscriptions

Never expose your subscription owner private key
Execute charges from your backend service that controls the subscription owner wallet:
chargeSubscriptions.ts
import { base } from '@base-org/account';
import { createWalletClient, http } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { base as baseChain } from 'viem/chains';

// Initialize wallet client with your subscription owner account
const account = privateKeyToAccount('0x...'); // Your app's private key
const walletClient = createWalletClient({
  account,
  chain: baseChain,
  transport: http()
});

async function chargeSubscription(subscriptionId: string) {
  try {
    // 1. Check subscription status
const status = await base.subscription.getStatus({
      id: subscriptionId,
      testnet: false
    });
    
    if (!status.isSubscribed) {
      console.log('Subscription cancelled by user');
      return { success: false, reason: 'cancelled' };
    }
    
    const availableCharge = parseFloat(status.remainingChargeInPeriod || '0');
    
    if (availableCharge === 0) {
      console.log(`No charge available until ${status.nextPeriodStart}`);
      return { success: false, reason: 'no_charge_available' };
    }
    
    // 2. Prepare charge transaction for max available amount
    const chargeCalls = await base.subscription.prepareCharge({
      id: subscriptionId,
      amount: 'max-remaining-charge',
      testnet: false
    });
    
    // 3. Execute each charge call using standard sendTransaction
    // Note: prepareCharge may return multiple calls (e.g., approval + transfer)
    // We execute them sequentially to ensure proper ordering
    const transactionHashes = [];
    
    for (const call of chargeCalls) {
      const hash = await walletClient.sendTransaction({
        to: call.to,
        data: call.data,
        value: call.value || 0n
      });
      
      transactionHashes.push(hash);
      
      // Wait for transaction confirmation before next call
      await walletClient.waitForTransactionReceipt({ hash });
    }
    
    
    console.log(`Charged ${availableCharge} USDC`);
    console.log(`Transactions: ${transactionHashes.join(', ')}`);
    
    return {
      success: true,
      transactionHashes,
      amount: availableCharge
    };
    
  } catch (error) {
    console.error('Charge failed:', error);
    return { success: false, error: error.message };
  }
}

Testing on Testnet

Test your subscription implementation on Base Sepolia before going live:
// Use testnet for development
const subscription = await base.subscription.subscribe({
  recurringCharge: "10.00",
  subscriptionOwner: "0xTestWallet",
  periodInDays: 1, // Daily for faster testing
  testnet: true     // Use Base Sepolia
});

// Check status on testnet
const status = await base.subscription.getStatus({
  id: subscription.id,
  testnet: true
});

// Prepare charge on testnet
const calls = await base.subscription.prepareCharge({
  id: subscription.id,
  amount: "10.00",
  testnet: true
});

Network Support

NetworkChain IDStatus
Base Mainnet8453✅ Production Ready
Base Sepolia84532✅ Testing Available

Type Definitions

// Subscription creation options
interface SubscriptionOptions {
  recurringCharge: string;
  subscriptionOwner: string;
  periodInDays?: number;
  testnet?: boolean;
  telemetry?: boolean;
}

// Subscription result
interface SubscriptionResult {
  id: string;
  subscriptionOwner: Address;
  subscriptionPayer: Address;
  recurringCharge: string;
  periodInDays: number;
}

// Subscription status
interface SubscriptionStatus {
  isSubscribed: boolean;
  recurringCharge: string;
  remainingChargeInPeriod?: string;
  currentPeriodStart?: Date;
  nextPeriodStart?: Date;
  periodInDays?: number;
}

// Charge preparation
interface PrepareChargeOptions {
  id: string;
  amount: string | 'max-remaining-charge';
  testnet?: boolean;
}

type PrepareChargeResult = Array<{
  to: Address;
  data: Hex;
  value: '0x0';
}>;

Next Steps