> ## Documentation Index
> Fetch the complete documentation index at: https://docs.base.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Accept Recurring Payments

> Enable subscription-based revenue models with automatic USDC payments

export const Button = ({children, disabled, variant = "primary", size = "medium", iconName, roundedFull = false, className = '', fullWidth = false, onClick = undefined}) => {
  const variantStyles = {
    primary: 'bg-blue text-black border border-blue hover:bg-blue-80 active:bg-[#06318E] dark:text-white',
    secondary: 'bg-white border border-white text-palette-foreground hover:bg-zinc-15 active:bg-zinc-30',
    outlined: 'bg-transparent text-white border border-white hover:bg-white hover:text-black active:bg-[#E3E7E9]'
  };
  const sizeStyles = {
    medium: 'text-md px-4 py-2 gap-3',
    large: 'text-lg px-6 py-4 gap-5'
  };
  const sizeIconRatio = {
    medium: '0.75rem',
    large: '1rem'
  };
  const classes = ['text-md px-4 py-2 whitespace-nowrap', 'flex items-center justify-center', 'disabled:opacity-40 disabled:pointer-events-none', 'transition-all', variantStyles[variant], sizeStyles[size], roundedFull ? 'rounded-full' : 'rounded-lg', fullWidth ? 'w-full' : 'w-auto', className];
  const buttonClasses = classes.filter(Boolean).join(' ');
  const iconSize = sizeIconRatio[size];
  return <button type="button" disabled={disabled} className={buttonClasses} onClick={onClick}>
      <span>{children}</span>
      {iconName && <Icon name={iconName} width={iconSize} height={iconSize} color="currentColor" />}
    </button>;
};

export const BaseBanner = ({content = null, id, dismissable = true}) => {
  const LOCAL_STORAGE_KEY_PREFIX = 'cb-docs-banner';
  const [isVisible, setIsVisible] = useState(false);
  const onDismiss = () => {
    localStorage.setItem(`${LOCAL_STORAGE_KEY_PREFIX}-${id}`, 'false');
    setIsVisible(false);
  };
  useEffect(() => {
    const storedValue = localStorage.getItem(`${LOCAL_STORAGE_KEY_PREFIX}-${id}`);
    setIsVisible(storedValue !== 'false');
  }, []);
  if (!isVisible) {
    return null;
  }
  return <div className="fixed bottom-0 left-0 right-0 bg-white py-8 px-4 lg:px-12 z-50 text-black dark:bg-black dark:text-white border-t dark:border-gray-95">
      <div className="flex items-center max-w-8xl mx-auto">
        {typeof content === 'function' ? content({
    onDismiss
  }) : content}
        {dismissable && <button onClick={onDismiss} className="flex-shrink-0 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors" aria-label="Dismiss banner">
          ✕
        </button>}
      </div>
    </div>;
};

export const SignInWithBaseButton = ({colorScheme = 'light'}) => {
  const isLight = colorScheme === 'light';
  return <button type="button" style={{
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'center',
    gap: '8px',
    padding: '12px 16px',
    backgroundColor: isLight ? '#ffffff' : '#000000',
    border: 'none',
    borderRadius: '8px',
    cursor: 'pointer',
    fontFamily: 'system-ui, -apple-system, sans-serif',
    fontSize: '14px',
    fontWeight: '500',
    color: isLight ? '#000000' : '#ffffff',
    minWidth: '180px',
    height: '44px'
  }}>
      <div style={{
    width: '16px',
    height: '16px',
    backgroundColor: isLight ? '#0000FF' : '#FFFFFF',
    borderRadius: '2px',
    flexShrink: 0
  }} />
      <span>Sign in with Base</span>
    </button>;
};

export const BasePayButton = ({colorScheme = 'light'}) => {
  const isLight = colorScheme === 'light';
  return <button type="button" style={{
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'center',
    padding: '12px 16px',
    backgroundColor: isLight ? '#ffffff' : '#0000FF',
    border: 'none',
    borderRadius: '8px',
    cursor: 'pointer',
    fontFamily: 'system-ui, -apple-system, sans-serif',
    minWidth: '180px',
    height: '44px'
  }}>
      <img src={isLight ? '/images/base-account/BasePayBlueLogo.png' : '/images/base-account/BasePayWhiteLogo.png'} alt="Base Pay" style={{
    height: '20px',
    width: 'auto'
  }} />
    </button>;
};

## 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:**

<AccordionGroup>
  <Accordion title="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
  </Accordion>

  <Accordion title="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
  </Accordion>

  <Accordion title="Subscription Management">
    Full control over the subscription lifecycle:

    * Real-time status checking to verify active subscriptions
    * Remaining charge amount for the current period
    * Next period start date for planning
    * Cancellation detection for immediate updates
  </Accordion>

  <Accordion title="Enterprise-Ready Features">
    Built for production use cases:

    * No transaction fees or platform cuts
    * Instant settlement in USDC stablecoin
    * Testnet support for development and testing
    * Detailed transaction history for accounting
    * Programmatic access via SDK
  </Accordion>
</AccordionGroup>

## How It Works

Base Subscriptions leverage **Spend Permissions** – a powerful onchain primitive that allows users to grant revocable spending rights to applications. Here's the complete flow:

<Steps>
  <Step title="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.
  </Step>

  <Step title="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.
  </Step>

  <Step title="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.
  </Step>

  <Step title="User Maintains Control">
    Customers can view and cancel their subscriptions anytime through their wallet, ensuring transparency and trust.
  </Step>
</Steps>

## Implementation Guide

### Architecture Overview

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

<Note>
  **CDP-Powered Backend**

  Base 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

  Get CDP credentials from [CDP Portal](https://portal.cdp.coinbase.com/projects/api-keys).
</Note>

<Warning>
  **Security Requirements**

  To accept recurring payments, you need:

  1. CDP credentials (API key ID, secret, and wallet secret)
  2. Backend infrastructure (Node.js) to execute charges securely
  3. Database to store and manage subscription IDs
  4. Never expose CDP credentials in client-side code
</Warning>

### Setup: Create Your Subscription Owner Wallet

First, set up your CDP smart wallet that will act as the subscription owner:

```typescript backend/setup.ts expandable theme={null}
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 startup
setupSubscriptionWallet();

// Optional: Provide an API endpoint for the frontend to fetch the address
export async function getSubscriptionOwnerAddress() {
  const wallet = await base.subscription.getOrCreateSubscriptionOwnerWallet();
  return wallet.address;
}
```

<Note>
  **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.
</Note>

<Warning>
  **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.
</Warning>

### Client-Side: Create Subscriptions

Users create subscriptions from your frontend application:

```tsx SubscriptionButton.tsx expandable theme={null}
import React, { useState } from 'react';
import { base } from '@base-org/account';

// This address comes from your backend setup (see setup.ts example above)
// You can fetch it from your backend or configure it as a public env var
const SUBSCRIPTION_OWNER_ADDRESS = "0xYourCDPWalletAddress"; // Replace with your actual address

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: SUBSCRIPTION_OWNER_ADDRESS, // Address from your backend CDP wallet
        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

Execute charges effortlessly from your backend using CDP:

```typescript chargeSubscriptions.ts expandable theme={null}
import { base } from '@base-org/account/node';

// Requires: CDP_API_KEY_ID, CDP_API_KEY_SECRET, CDP_WALLET_SECRET env vars
// Recommended: PAYMASTER_URL for gasless transactions

async function chargeSubscription(subscriptionId: string, recipientAddress?: 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. Charge the subscription - CDP handles everything automatically
    // Using paymaster for gasless transactions (recommended)
    const result = await base.subscription.charge({
      id: subscriptionId,
      amount: 'max-remaining-charge',
      paymasterUrl: process.env.PAYMASTER_URL, // Optional: for gasless transactions
      recipient: recipientAddress, // Optional: send USDC to specific address
      testnet: false
    });
    
    console.log(`✅ Charged ${result.amount} USDC (gasless)`);
    console.log(`Transaction: ${result.id}`);
    if (recipientAddress) {
      console.log(`Sent to: ${recipientAddress}`);
    }
    
    return {
      success: true,
      transactionHash: result.id,
      amount: result.amount,
      recipient: result.recipient
    };
    
  } catch (error) {
    console.error('Charge failed:', error);
    return { success: false, error: error.message };
  }
}
```

### Server-Side: Revoke Subscriptions

Cancel subscriptions programmatically from your backend:

```typescript revokeSubscription.ts expandable theme={null}
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 examples
async function handleUserCancellation(subscriptionId: string) {
  return await revokeSubscription(subscriptionId, 'user_requested');
}

async function handlePolicyViolation(subscriptionId: string) {
  return await revokeSubscription(subscriptionId, 'policy_violation');
}
```

<Note>
  **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.
</Note>

<Tip>
  **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](https://portal.cdp.coinbase.com/).
</Tip>

### Fund Management

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:

<Tabs>
  <Tab title="Default (Keep in Owner Wallet)">
    ```typescript theme={null}
    // Funds stay in the subscription owner wallet
    const 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
    ```
  </Tab>

  <Tab title="Send to Treasury Wallet">
    ```typescript theme={null}
    // Automatically send to your treasury wallet
    const result = await base.subscription.charge({
      id: subscriptionId,
      amount: 'max-remaining-charge',
      recipient: '0xYourTreasuryAddress',
      testnet: false
    });

    // USDC is sent directly to the recipient address
    console.log(`Sent ${result.amount} to ${result.recipient}`);
    ```
  </Tab>

  <Tab title="Dynamic Recipients">
    ```typescript theme={null}
    // Send to different addresses based on subscription type
    async 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
      });
    }
    ```
  </Tab>
</Tabs>

### Testing on Testnet

Test your subscription implementation on Base Sepolia before going live:

```typescript testnet-frontend.ts expandable theme={null}
// Frontend: Create subscription on testnet
const subscription = await base.subscription.subscribe({
  recurringCharge: "10.00",
  subscriptionOwner: SUBSCRIPTION_OWNER_ADDRESS,
  periodInDays: 1, // Daily for faster testing
  testnet: true     // Use Base Sepolia
});
```

```typescript testnet-backend.ts expandable theme={null}
// Backend: Setup wallet on testnet (Node.js only)
import { base } from '@base-org/account/node';

const wallet = await base.subscription.getOrCreateSubscriptionOwnerWallet({
  walletName: 'testnet-subscriptions'
});

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

// Charge on testnet with paymaster
const result = await base.subscription.charge({
  id: subscriptionId,
  amount: "10.00",
  paymasterUrl: process.env.PAYMASTER_URL, // Gasless transactions
  testnet: true
});

console.log(`Testnet charge (gasless): ${result.id}`);
```

## Network and Token Support

**Base Subscriptions (USDC on Base):**

| Network      | Chain ID | Token | Status              |
| ------------ | -------- | ----- | ------------------- |
| Base Mainnet | 8453     | USDC  | ✅ Production Ready  |
| Base Sepolia | 84532    | USDC  | ✅ Testing Available |

<Note>
  **Custom Implementations Possible**: While Base Subscriptions are optimized for USDC on Base, you can use the underlying [Spend Permissions](/base-account/improve-ux/spend-permissions) primitive to build custom subscription implementations with any ERC-20 token or native ETH on any EVM-compatible chain.
</Note>

## Advanced Topics

### Custom Transaction Handling

For developers who need manual control over transaction execution or want to integrate with existing wallet infrastructure, use the lower-level utilities:

<AccordionGroup>
  <Accordion title="prepareCharge - Manual Charge Execution">
    If you can't use CDP wallets, `prepareCharge()` gives you call data to execute manually:

    ```typescript theme={null}
    import { base } from '@base-org/account';

    // Prepare charge call data
    const chargeCalls = await base.subscription.prepareCharge({
      id: subscriptionId,
      amount: 'max-remaining-charge',
      testnet: false
    });

    // Execute with your own wallet infrastructure
    // (requires custom wallet client setup)
    ```

    See [`prepareCharge` reference](/base-account/reference/base-pay/prepareCharge) for details.
  </Accordion>

  <Accordion title="prepareRevoke - Manual Revoke Execution">
    Similarly, `prepareRevoke()` provides revocation call data:

    ```typescript theme={null}
    import { base } from '@base-org/account';

    // Prepare revoke call data
    const revokeCall = await base.subscription.prepareRevoke({
      id: subscriptionId,
      testnet: false
    });

    // Execute with your own wallet infrastructure
    ```

    See [`prepareRevoke` reference](/base-account/reference/base-pay/prepareRevoke) for details.
  </Accordion>
</AccordionGroup>

## API Reference

<CardGroup cols={3}>
  <Card title="subscribe()" icon="credit-card" href="/base-account/reference/base-pay/subscribe">
    Create subscriptions from frontend
  </Card>

  <Card title="getStatus()" icon="chart-line" href="/base-account/reference/base-pay/getStatus">
    Check subscription status
  </Card>

  <Card title="charge()" icon="bolt" href="/base-account/reference/base-pay/charge">
    Charge subscriptions from backend
  </Card>

  <Card title="revoke()" icon="ban" href="/base-account/reference/base-pay/revoke">
    Cancel subscriptions from backend
  </Card>

  <Card title="Setup Owner Wallet" icon="wallet" href="/base-account/reference/base-pay/getOrCreateSubscriptionOwnerWallet">
    Setup CDP owner wallet for subscription management
  </Card>

  <Card title="prepareCharge()" icon="code" href="/base-account/reference/base-pay/prepareCharge">
    Advanced: Custom charge execution
  </Card>

  <Card title="prepareRevoke()" icon="code" href="/base-account/reference/base-pay/prepareRevoke">
    Advanced: Custom revoke execution
  </Card>

  <Card title="Spend Permissions" icon="shield" href="/base-account/improve-ux/spend-permissions">
    Deep dive into the underlying primitive
  </Card>

  <Card title="One-Time Payments" icon="credit-card" href="/base-account/guides/accept-payments">
    Accept single payments
  </Card>
</CardGroup>

<BaseBanner
  id="privacy-policy"
  dismissable={false}
  content={({ onDismiss }) => (
<div className="flex items-center">
  <div className="mr-2">
    We're updating the Base Privacy Policy, effective July 25, 2025, to reflect an expansion of Base services. Please review the updated policy here:{" "}
    <a
      href="https://docs.base.org/privacy-policy-2025"
      target="_blank"
      className="whitespace-nowrap"
    >
      Base Privacy Policy
    </a>. By continuing to use Base services, you confirm that you have read and understand the updated policy.
  </div>
  <Button onClick={onDismiss}>I Acknowledge</Button>
</div>
)}
/>
