Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 25 additions & 9 deletions docs/base-account/guides/accept-recurring-payments.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,9 @@ Get CDP credentials from [CDP Portal](https://portal.cdp.coinbase.com/projects/a
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
3. Database to store subscription IDs **bound to the authenticated user** (and their payer address)
4. Never expose CDP credentials in client-side code
5. Never charge an arbitrary subscription `id` from the browser alone — look up the ID from your database, and pass `expectedPayer` to `charge()` / `revoke()`
</Warning>

### Setup: Create Your Subscription Owner Wallet
Expand Down Expand Up @@ -256,7 +257,11 @@ 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) {
async function chargeSubscription(
subscriptionId: string,
payerAddress: string,
recipientAddress?: string
) {
try {
// 1. Check subscription status
const status = await base.subscription.getStatus({
Expand All @@ -277,10 +282,12 @@ async function chargeSubscription(subscriptionId: string, recipientAddress?: str
}

// 2. Charge the subscription - CDP handles everything automatically
// Use the server-stored id + payerAddress from your database (not browser input alone)
// Using paymaster for gasless transactions (recommended)
const result = await base.subscription.charge({
id: subscriptionId,
amount: 'max-remaining-charge',
expectedPayer: payerAddress,
paymasterUrl: process.env.PAYMASTER_URL, // Optional: for gasless transactions
recipient: recipientAddress, // Optional: send USDC to specific address
testnet: false
Expand Down Expand Up @@ -313,11 +320,16 @@ Cancel subscriptions programmatically from your backend:
```typescript revokeSubscription.ts expandable
import { base } from '@base-org/account/node';

async function revokeSubscription(subscriptionId: string, reason: string) {
async function revokeSubscription(
subscriptionId: string,
payerAddress: string,
reason: string
) {
try {
// Revoke the subscription with paymaster for gasless transactions
const result = await base.subscription.revoke({
id: subscriptionId,
expectedPayer: payerAddress,
paymasterUrl: process.env.PAYMASTER_URL, // Optional: for gasless transactions
testnet: false
});
Expand All @@ -338,12 +350,12 @@ async function revokeSubscription(subscriptionId: string, reason: string) {
}

// Usage examples
async function handleUserCancellation(subscriptionId: string) {
return await revokeSubscription(subscriptionId, 'user_requested');
async function handleUserCancellation(subscriptionId: string, payerAddress: string) {
return await revokeSubscription(subscriptionId, payerAddress, 'user_requested');
}

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

Expand Down Expand Up @@ -479,7 +491,9 @@ import { base } from '@base-org/account';
const chargeCalls = await base.subscription.prepareCharge({
id: subscriptionId,
amount: 'max-remaining-charge',
testnet: false
testnet: false,
expectedSpender: subscriptionOwner,
expectedPayer: payerAddress,
});

// Execute with your own wallet infrastructure
Expand All @@ -498,7 +512,9 @@ import { base } from '@base-org/account';
// Prepare revoke call data
const revokeCall = await base.subscription.prepareRevoke({
id: subscriptionId,
testnet: false
testnet: false,
expectedSpender: subscriptionOwner,
expectedPayer: payerAddress,
});

// Execute with your own wallet infrastructure
Expand Down
38 changes: 33 additions & 5 deletions docs/base-account/reference/base-pay/charge.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,24 @@ Defined in the [Base Account SDK](https://github.com/base/account-sdk)
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
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()`.
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>
Expand Down Expand Up @@ -69,6 +73,18 @@ Optional recipient address to receive the charged USDC. If not provided, USDC st
**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">
Expand Down Expand Up @@ -119,9 +135,11 @@ Or pass directly as parameters (see examples below).
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: '0x71319cd488f8e4f24687711ec5c95d9e0c1bacbf5c1064942937eba4c7cf2984',
amount: '9.99'
id: storedSubscriptionId,
amount: '9.99',
expectedPayer: authenticatedUserAddress,
});

console.log(`Charged subscription: ${result.id}`);
Expand Down Expand Up @@ -286,6 +304,15 @@ 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
Expand Down Expand Up @@ -320,10 +347,11 @@ async function chargeActiveSubscriptions() {
continue;
}

// Execute charge
// 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
});

Expand Down
24 changes: 21 additions & 3 deletions docs/base-account/reference/base-pay/prepareCharge.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ Defined in the [Base Account SDK](https://github.com/base/account-sdk)
The `prepareCharge` function prepares the necessary transaction calls to charge a subscription. It returns the array of call data objects to execute the charge through `wallet_sendCalls` or `eth_sendTransaction`. This gives you programmatic control over when and how to execute subscription charges.
</Info>

<Warning>
A subscription ID is not an authorization token. Store IDs server-side against the authenticated user, and pass `expectedSpender` / `expectedPayer` when the ID may come from an untrusted client. Execute the returned calls from the subscription owner (`from` must be the spender).
</Warning>

## When to Use This

Use `prepareCharge` only if you need:
Expand All @@ -26,7 +30,7 @@ For standard backend subscription management, use [`charge()`](/base-account/ref
## Parameters

<ParamField body="id" type="string" required>
The subscription ID (permission hash) returned from subscribe().
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>
Expand All @@ -39,6 +43,18 @@ Amount to charge (e.g., "10.50") or 'max-remaining-charge' for the full remainin
Must match the testnet setting used in the original subscribe call. Default: false
</ParamField>

<ParamField body="expectedSpender" type="string">
Optional address that must match the subscription spender (subscription owner). Pass your app's spender wallet before executing the calls.

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

<ParamField body="expectedPayer" type="string">
Optional address that must match the subscription payer (subscriber). Pass the authenticated user's wallet from your session or database.

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

## Returns

<ResponseField name="result" type="PrepareChargeResult">
Expand Down Expand Up @@ -80,9 +96,11 @@ const walletClient = createWalletClient({

// Prepare to charge a specific amount
const chargeCalls = await base.subscription.prepareCharge({
id: '0x71319cd488f8e4f24687711ec5c95d9e0c1bacbf5c1064942937eba4c7cf2984',
id: storedSubscriptionId,
amount: '9.99',
testnet: false
testnet: false,
expectedSpender: account.address,
expectedPayer: authenticatedUserAddress,
});

// Execute each charge call
Expand Down
18 changes: 17 additions & 1 deletion docs/base-account/reference/base-pay/prepareRevoke.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ Defined in the [Base Account SDK](https://github.com/base/account-sdk)
The `prepareRevoke` function prepares the necessary transaction call to revoke a subscription. It returns call data that you can execute through your own wallet infrastructure using `wallet_sendCalls` or `eth_sendTransaction`.
</Info>

<Warning>
A subscription ID is not an authorization token. Prefer server-stored IDs bound to the authenticated user, and pass `expectedSpender` / `expectedPayer` when the ID may be untrusted.
</Warning>

## When to Use This

Use `prepareRevoke` only if you need:
Expand All @@ -25,7 +29,7 @@ For standard backend subscription management, use [`revoke()`](/base-account/ref
## Parameters

<ParamField body="id" type="string" required>
The subscription ID (permission hash) returned from `subscribe()`.
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>
Expand All @@ -34,6 +38,18 @@ The subscription ID (permission hash) returned from `subscribe()`.
Must match the testnet setting used in the original subscribe call. Default: false
</ParamField>

<ParamField body="expectedSpender" type="string">
Optional address that must match the subscription spender (subscription owner).

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

<ParamField body="expectedPayer" type="string">
Optional address that must match the subscription payer (subscriber).

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

## Returns

<ResponseField name="result" type="PrepareRevokeResult">
Expand Down
23 changes: 20 additions & 3 deletions docs/base-account/reference/base-pay/revoke.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,24 @@ Defined in the [Base Account SDK](https://github.com/base/account-sdk)
The `revoke` function cancels subscriptions automatically from your backend. It uses a CDP smart wallet as the subscription owner to execute the revocation transaction, handling all details including wallet management, transaction signing, and optional gas sponsorship.
</Info>

<Warning>
**Treat subscription IDs as capability handles, not proof of ownership.** Prefer a server-stored ID bound to the authenticated user. Pass `expectedPayer` so the SDK rejects revocations for subscriptions that do not belong to that user. `revoke()` also requires the permission spender to match your CDP smart wallet.
</Warning>

## How It Works

When you call `revoke()`, the function:
1. Initializes a CDP client with your credentials
2. Retrieves the existing smart wallet (subscription owner)
3. Prepares the revoke transaction call
3. Prepares the revoke transaction call (and verifies the permission spender matches that wallet)
4. Executes the revocation 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()`.
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>
Expand Down Expand Up @@ -59,6 +63,18 @@ Optional custom wallet name for the CDP smart wallet. Default: "subscription own
Paymaster URL for transaction sponsorship (gasless transactions). Falls back to `PAYMASTER_URL` environment variable.
</ParamField>

<ParamField body="expectedPayer" type="string">
Optional subscriber wallet address. When set, the subscription's payer must match this address or `revoke()` throws.

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

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

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

## Returns

<ResponseField name="result" type="RevokeResult">
Expand Down Expand Up @@ -102,7 +118,8 @@ import { base } from '@base-org/account/node';

// Requires: CDP_API_KEY_ID, CDP_API_KEY_SECRET, CDP_WALLET_SECRET env vars
const result = await base.subscription.revoke({
id: '0x71319cd488f8e4f24687711ec5c95d9e0c1bacbf5c1064942937eba4c7cf2984'
id: storedSubscriptionId,
expectedPayer: authenticatedUserAddress,
});

console.log(`Revoked subscription: ${result.id}`);
Expand Down
10 changes: 10 additions & 0 deletions docs/base-account/reference/base-pay/subscriptions-overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ interface ChargeOptions {
paymasterUrl?: string;
recipient?: Address;
testnet?: boolean;
/** Subscriber wallet; rejects if the permission payer does not match */
expectedPayer?: Address;
/** Optional; charge() already binds spender to the CDP smart wallet */
expectedSpender?: Address;
}

// Charge result
Expand All @@ -112,6 +116,8 @@ interface RevokeOptions {
id: string;
paymasterUrl?: string;
testnet?: boolean;
expectedPayer?: Address;
expectedSpender?: Address;
}

// Revoke result
Expand All @@ -135,6 +141,8 @@ interface PrepareChargeOptions {
amount: string | 'max-remaining-charge';
recipient?: Address;
testnet?: boolean;
expectedSpender?: Address;
expectedPayer?: Address;
}

type PrepareChargeResult = Array<{
Expand All @@ -147,6 +155,8 @@ type PrepareChargeResult = Array<{
interface PrepareRevokeOptions {
id: string;
testnet?: boolean;
expectedSpender?: Address;
expectedPayer?: Address;
}

type PrepareRevokeResult = {
Expand Down
Loading