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

# subscription.charge

> Execute subscription charges from your backend using CDP server wallets

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>;
};

Defined in the [Base Account SDK](https://github.com/base/account-sdk)

<Note>
  **Node.js Only**: This function uses CDP (Coinbase Developer Platform) server wallets and is only available in Node.js environments. For browser/client-side applications, use [`prepareCharge`](/base-account/reference/base-pay/prepareCharge) instead.
</Note>

<Info>
  The `charge` function executes subscription charges automatically from your backend. It uses a CDP smart wallet as the subscription owner, handling all transaction details including wallet management, transaction signing, and optional gas sponsorship. **No manual transaction management required.**
</Info>

<Warning>
  **Treat subscription IDs as capability handles, not proof of ownership.** Store the ID on your backend against the authenticated user when they subscribe. Do not charge an arbitrary `id` supplied by the browser alone. Pass `expectedPayer` (the subscriber's wallet from your session/database) so the SDK rejects subscriptions that do not belong to that user. `charge()` also requires the permission spender to match your CDP smart wallet.
</Warning>

## How It Works

When you call `charge()`, the function:

1. Initializes a CDP client with your credentials
2. Retrieves the existing smart wallet (subscription owner)
3. Prepares the charge transaction calls (and verifies the permission spender matches that wallet)
4. Executes the charge using the smart wallet
5. Optionally uses a paymaster for gas sponsorship
6. Returns the transaction hash

## Parameters

<ParamField body="id" type="string" required>
  The subscription ID (permission hash) returned from `subscribe()`. Prefer a server-stored ID bound to the authenticated user.

  **Pattern:** `^0x[0-9a-fA-F]{64}$`
</ParamField>

<ParamField body="amount" type="string | 'max-remaining-charge'" required>
  Amount to charge as a string (e.g., "10.50") or `'max-remaining-charge'` to charge the full remaining amount in the current period.
</ParamField>

<ParamField body="testnet" type="boolean">
  Whether to use Base Sepolia testnet. Must match the network used in `subscribe()`. Default: false
</ParamField>

<ParamField body="cdpApiKeyId" type="string">
  CDP API key ID. Falls back to `CDP_API_KEY_ID` environment variable.
</ParamField>

<ParamField body="cdpApiKeySecret" type="string">
  CDP API key secret. Falls back to `CDP_API_KEY_SECRET` environment variable.
</ParamField>

<ParamField body="cdpWalletSecret" type="string">
  CDP wallet secret. Falls back to `CDP_WALLET_SECRET` environment variable.
</ParamField>

<ParamField body="walletName" type="string">
  Optional custom wallet name for the CDP smart wallet. Default: "subscription owner"

  <Note>
    **Use Default**: Most applications should omit this parameter and use the default. Only specify if you used a custom name in `getOrCreateSubscriptionOwnerWallet()`.
  </Note>
</ParamField>

<ParamField body="paymasterUrl" type="string">
  Paymaster URL for transaction sponsorship (gasless transactions). Falls back to `PAYMASTER_URL` environment variable.
</ParamField>

<ParamField body="recipient" type="string">
  Optional recipient address to receive the charged USDC. If not provided, USDC stays in the subscription owner wallet.

  **Pattern:** `^0x[0-9a-fA-F]{40}$`
</ParamField>

<ParamField body="expectedPayer" type="string">
  Optional subscriber wallet address. When set, the subscription's payer must match this address or `charge()` throws. Use the authenticated user's wallet from your session or database.

  **Pattern:** `^0x[0-9a-fA-F]{40}$`
</ParamField>

<ParamField body="expectedSpender" type="string">
  Optional. Must match your CDP smart wallet if provided. `charge()` already binds the permission spender to the executing CDP wallet automatically.

  **Pattern:** `^0x[0-9a-fA-F]{40}$`
</ParamField>

## Returns

<ResponseField name="result" type="ChargeResult">
  Charge execution result.

  <Expandable title="ChargeResult properties">
    <ResponseField name="success" type="true">
      Always true on successful charge.
    </ResponseField>

    <ResponseField name="id" type="string">
      Transaction hash of the charge.
    </ResponseField>

    <ResponseField name="subscriptionId" type="string">
      The subscription ID that was charged.
    </ResponseField>

    <ResponseField name="amount" type="string">
      Amount that was charged ("max" if using max-remaining-charge).
    </ResponseField>

    <ResponseField name="subscriptionOwner" type="Address">
      Address of the wallet that executed the charge.
    </ResponseField>

    <ResponseField name="recipient" type="Address">
      Recipient address (only present if specified in parameters).
    </ResponseField>
  </Expandable>
</ResponseField>

## Setup

Before using `charge()`, you need CDP credentials. Get them from the [CDP Portal](https://portal.cdp.coinbase.com/projects/api-keys).

Set as environment variables:

```bash theme={null}
export CDP_API_KEY_ID="your-api-key-id"
export CDP_API_KEY_SECRET="your-api-key-secret"
export CDP_WALLET_SECRET="your-wallet-secret"
```

Or pass directly as parameters (see examples below).

<RequestExample>
  ```typescript Basic Charge with Environment Variables theme={null}
  import { base } from '@base-org/account/node';

  // Requires: CDP_API_KEY_ID, CDP_API_KEY_SECRET, CDP_WALLET_SECRET env vars
  // Use a server-stored subscription id + the subscriber wallet from your DB/session
  const result = await base.subscription.charge({
    id: storedSubscriptionId,
    amount: '9.99',
    expectedPayer: authenticatedUserAddress,
  });

  console.log(`Charged subscription: ${result.id}`);
  console.log(`Amount: ${result.amount}`);
  ```

  ```typescript Charge with Explicit Credentials theme={null}
  import { base } from '@base-org/account/node';

  const result = await base.subscription.charge({
    id: '0x71319cd488f8e4f24687711ec5c95d9e0c1bacbf5c1064942937eba4c7cf2984',
    amount: '9.99',
    cdpApiKeyId: 'your-api-key-id',
    cdpApiKeySecret: 'your-api-key-secret',
    cdpWalletSecret: 'your-wallet-secret',
    testnet: false
  });

  console.log(`Charged subscription: ${result.id}`);
  ```

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

  // Charge the full remaining amount in current period
  const result = await base.subscription.charge({
    id: subscriptionId,
    amount: 'max-remaining-charge',
    testnet: false
  });

  console.log(`Charged max remaining: ${result.amount}`);
  ```

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

  // Charge and transfer USDC to a specific recipient
  const result = await base.subscription.charge({
    id: subscriptionId,
    amount: '10.00',
    recipient: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb8',
    testnet: false
  });

  console.log(`Charged and sent to ${result.recipient}`);
  ```

  ```typescript Charge with Paymaster (Gasless) theme={null}
  import { base } from '@base-org/account/node';

  // Use a paymaster to sponsor gas fees
  const result = await base.subscription.charge({
    id: subscriptionId,
    amount: '15.00',
    paymasterUrl: 'https://api.developer.coinbase.com/rpc/v1/base/your-key',
    testnet: false
  });

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

  ```typescript Advanced: Custom Wallet Name (If Needed) theme={null}
  import { base } from '@base-org/account/node';

  // Only needed if you used a custom name in getOrCreateSubscriptionOwnerWallet()
  const result = await base.subscription.charge({
    id: subscriptionId,
    amount: '5.00',
    walletName: 'premium-subscriptions-wallet', // Must match wallet creation
    testnet: false
  });
  ```
</RequestExample>

<ResponseExample>
  ```typescript Success Response theme={null}
  {
    success: true,
    id: "0x8f3d9e2a1b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e",
    subscriptionId: "0x71319cd488f8e4f24687711ec5c95d9e0c1bacbf5c1064942937eba4c7cf2984",
    amount: "9.99",
    subscriptionOwner: "0xYourSubscriptionOwnerWallet"
  }
  ```

  ```typescript Success with Recipient theme={null}
  {
    success: true,
    id: "0x8f3d9e2a1b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e",
    subscriptionId: "0x71319cd488f8e4f24687711ec5c95d9e0c1bacbf5c1064942937eba4c7cf2984",
    amount: "10.00",
    subscriptionOwner: "0xYourSubscriptionOwnerWallet",
    recipient: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb8"
  }
  ```
</ResponseExample>

## Error Handling

Always wrap `charge()` calls in try-catch blocks:

```typescript theme={null}
try {
  const result = await base.subscription.charge({
    id: subscriptionId,
    amount: 'max-remaining-charge',
    testnet: false
  });
  
  console.log(`Successfully charged: ${result.id}`);
  
  // Update your database with the transaction hash
  await updateSubscriptionRecord(subscriptionId, {
    lastCharge: new Date(),
    transactionHash: result.id
  });
  
} catch (error) {
  console.error(`Failed to charge subscription: ${error.message}`);
  
  // Handle specific error cases
  if (error.message.includes('not found')) {
    console.log('Subscription cancelled or not found');
  } else if (error.message.includes('CDP')) {
    console.log('CDP credentials issue');
  }
}
```

## Common Errors

<AccordionGroup>
  <Accordion title="Missing CDP Credentials">
    ```
    Failed to initialize CDP client for subscription charge
    ```

    **Solution**: Ensure `CDP_API_KEY_ID`, `CDP_API_KEY_SECRET`, and `CDP_WALLET_SECRET` are set as environment variables or passed as parameters.
  </Accordion>

  <Accordion title="Subscription Not Found">
    ```
    Subscription with ID 0x... not found
    ```

    **Solution**: Check that the subscription ID is correct and the subscription hasn't been cancelled. Use [`getSubscriptionStatus`](/base-account/reference/base-pay/getStatus) to verify.
  </Accordion>

  <Accordion title="No Remaining Charge">
    ```
    No charge available until [date]
    ```

    **Solution**: The subscription has been fully charged for the current period. Wait until the next period starts.
  </Accordion>

  <Accordion title="Wallet Does Not Exist">
    ```
    Wallet "subscription owner" does not exist
    ```

    **Solution**: The CDP wallet hasn't been created yet. First call [`getOrCreateSubscriptionOwnerWallet`](/base-account/reference/base-pay/getOrCreateSubscriptionOwnerWallet) to set up the wallet.
  </Accordion>

  <Accordion title="Spender or Payer Mismatch">
    ```
    Subscription spender 0x... does not match expected spender 0x...
    Subscription payer 0x... does not match expected payer 0x...
    ```

    **Solution**: Confirm you are charging with the CDP wallet that was used as `subscriptionOwner` at subscribe time, and that `expectedPayer` matches the subscriber wallet stored for that user.
  </Accordion>
</AccordionGroup>

## Usage Pattern

Typical implementation in a backend service:

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

async function chargeActiveSubscriptions() {
  // Get all subscriptions due for charging from your database
  const subscriptions = await db.getSubscriptionsDueForCharge();
  
  for (const subscription of subscriptions) {
    try {
      // Check status first
      const status = await base.subscription.getStatus({
        id: subscription.subscriptionId,
        testnet: false
      });
      
      if (!status.isSubscribed) {
        console.log(`Subscription ${subscription.id} cancelled`);
        await db.markSubscriptionCancelled(subscription.id);
        continue;
      }
      
      const remainingCharge = parseFloat(status.remainingChargeInPeriod || '0');
      
      if (remainingCharge === 0) {
        console.log(`No charge available for ${subscription.id}`);
        continue;
      }
      
      // Execute charge — pass expectedPayer from your DB record
      const result = await base.subscription.charge({
        id: subscription.subscriptionId,
        amount: 'max-remaining-charge',
        expectedPayer: subscription.payerAddress,
        testnet: false
      });
      
      // Update database
      await db.recordCharge({
        subscriptionId: subscription.id,
        transactionHash: result.id,
        amount: result.amount,
        chargedAt: new Date()
      });
      
      console.log(`✅ Charged subscription ${subscription.id}: ${result.amount}`);
      
    } catch (error) {
      console.error(`Failed to charge ${subscription.id}:`, error.message);
      
      // Log error for monitoring
      await db.logChargeError({
        subscriptionId: subscription.id,
        error: error.message,
        timestamp: new Date()
      });
    }
  }
}

// Run every hour
setInterval(chargeActiveSubscriptions, 60 * 60 * 1000);
```

## Related Functions

<CardGroup cols={3}>
  <Card title="Setup Wallet" icon="wallet" href="/base-account/reference/base-pay/getOrCreateSubscriptionOwnerWallet">
    Create CDP wallet before charging
  </Card>

  <Card title="Check Status" icon="chart-line" href="/base-account/reference/base-pay/getStatus">
    Verify subscription before charging
  </Card>

  <Card title="Custom Charge" icon="code" href="/base-account/reference/base-pay/prepareCharge">
    Advanced manual execution
  </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>
)}
/>
