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

# 정기 결제 수락

> 자동 USDC 결제로 구독 기반 수익 모델을 활성화하는 방법을 알아보세요.

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

## Base Pay 구독으로 정기 결제 수락 시작하기

Base 구독을 통해 자동 USDC 결제를 수락하여 예측 가능한 반복 수익 흐름을 구축할 수 있습니다. SaaS 플랫폼, 콘텐츠 구독 서비스, 또는 정기 결제가 필요한 비즈니스 모델을 운영하든 관계없이, Base 구독은 가맹점 수수료 없이 원활한 솔루션을 제공합니다.

**주요 기능:**

<AccordionGroup>
  <Accordion title="유연한 결제 주기">
    비즈니스 모델에 맞는 모든 청구 주기를 지원합니다:

    * 단기 서비스를 위한 일별 구독
    * 정기 배달 또는 서비스를 위한 주별
    * 표준 SaaS 구독을 위한 월별
    * 할인된 장기 약정을 위한 연간
    * 고유한 모델을 위한 커스텀 기간 (예: 14일, 90일)
  </Accordion>

  <Accordion title="부분 및 사용량 기반 청구">
    허용된 한도 내에서 모든 금액을 청구할 수 있습니다:

    * 예측 가능한 청구를 위한 고정 반복 금액
    * 한도 내 가변 사용량 기반 요금
    * 다양한 청구 금액을 가진 계층형 요금제
    * 주기 중간 변경을 위한 비례 배분 요금
  </Accordion>

  <Accordion title="구독 관리">
    구독 생명주기에 대한 완전한 제어:

    * 활성 구독 확인을 위한 실시간 상태 확인
    * 현재 기간의 남은 청구 금액
    * 계획 수립을 위한 다음 기간 시작일
    * 즉각적인 업데이트를 위한 취소 감지
  </Accordion>

  <Accordion title="엔터프라이즈급 기능">
    프로덕션 사용 사례를 위해 설계됨:

    * 트랜잭션 수수료 또는 플랫폼 수수료 없음
    * USDC 스테이블코인으로 즉시 정산
    * 개발 및 테스트를 위한 테스트넷 지원
    * 회계를 위한 상세 트랜잭션 내역
    * SDK를 통한 프로그래밍 방식 접근
  </Accordion>
</AccordionGroup>

## 작동 방식

Base 구독은 사용자가 애플리케이션에 취소 가능한 지출 권한을 부여하는 강력한 온체인 프리미티브인 \*\*스펜드 퍼미션(Spend Permissions)\*\*을 활용합니다. 전체 흐름은 다음과 같습니다:

<Steps>
  <Step title="사용자가 구독 승인">
    고객이 매 청구 기간마다 지정된 금액까지 지갑을 청구할 수 있는 권한을 애플리케이션에 부여합니다. 이는 취소될 때까지 유지되는 일회성 승인입니다.
  </Step>

  <Step title="애플리케이션이 주기적으로 청구">
    백엔드 서비스가 사용자 상호작용 없이 결제 기한이 됐을 때 구독을 청구합니다. 기간당 승인된 금액까지 청구할 수 있습니다.
  </Step>

  <Step title="스마트 기간 관리">
    지출 한도는 각 새 기간 시작 시 자동으로 초기화됩니다. 한 기간에 전액을 청구하지 않아도 이월되지 않습니다.
  </Step>

  <Step title="사용자가 제어권 유지">
    고객은 언제든지 지갑을 통해 구독을 확인하고 취소할 수 있어 투명성과 신뢰를 보장합니다.
  </Step>
</Steps>

## 구현 가이드

### 아키텍처 개요

완전한 구독 구현에는 클라이언트와 서버 컴포넌트가 모두 필요합니다:

**클라이언트 측 (프론트엔드):**

* 구독 생성을 위한 사용자 인터페이스
* 지갑 요청 생성 및 사용자 응답 처리

**서버 측 (백엔드 - Node.js):**

* 청구 및 취소 실행을 위한 CDP 스마트 지갑
* 주기적 청구를 위한 스케줄링 작업
* 구독 추적을 위한 데이터베이스
* 상태 업데이트 핸들러
* 실패한 청구를 위한 재시도 로직

<Note>
  **CDP 기반 백엔드**

  Base 구독은 손쉬운 백엔드 관리를 위해 **CDP(Coinbase Developer Platform) 서버 지갑**을 사용합니다. `charge()`와 `revoke()` 함수가 모든 트랜잭션 세부 사항을 자동으로 처리합니다:

  * ✅ 자동 지갑 관리
  * ✅ 내장된 트랜잭션 서명
  * ✅ 가스 추정 및 논스 처리
  * ✅ 가스리스 트랜잭션을 위한 선택적 페이마스터 지원

  [CDP Portal](https://portal.cdp.coinbase.com/projects/api-keys)에서 CDP 자격 증명을 받으세요.
</Note>

<Warning>
  **보안 요구 사항**

  정기 결제를 수락하려면 다음이 필요합니다:

  1. CDP 자격 증명 (API 키 ID, 시크릿, 지갑 시크릿)
  2. 청구를 안전하게 실행하기 위한 백엔드 인프라 (Node.js)
  3. 구독 ID 저장 및 관리를 위한 데이터베이스
  4. 클라이언트 측 코드에 CDP 자격 증명을 절대 노출하지 마세요
</Warning>

### 설정: 구독 소유자 지갑 생성

먼저 구독 소유자 역할을 할 CDP 스마트 지갑을 설정하세요:

```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>
  **백엔드 전용**: 이 설정은 CDP 자격 증명을 가진 Node.js 백엔드에서 실행됩니다. 결과 지갑 주소는 공개적이며 `subscribe()` 호출 시 사용하기 위해 프론트엔드에 안전하게 공유할 수 있습니다.
</Note>

<Warning>
  **CDP 자격 증명을 비공개로 유지하세요**: CDP 자격 증명(API 키, 시크릿)을 프론트엔드에 절대 노출하지 마세요. 구독 소유자 지갑 주소만 프론트엔드에서 접근 가능해야 합니다.
</Warning>

### 클라이언트 측: 구독 생성

사용자는 프론트엔드 애플리케이션에서 구독을 생성합니다:

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

### 서버 측: 구독 청구

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

### 서버 측: 구독 취소

백엔드에서 프로그래밍 방식으로 구독을 취소합니다:

```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>
  **자동 트랜잭션 관리**: `charge()`와 `revoke()` 함수는 지갑 관리, 가스 추정, 논스 처리, 트랜잭션 확인을 포함한 모든 트랜잭션 세부 사항을 처리합니다. 사용자를 위한 가스리스 트랜잭션을 활성화하려면 `paymasterUrl` 파라미터를 사용하세요.
</Note>

<Tip>
  **가스리스 트랜잭션**: 구독 청구 및 취소에 대한 가스 수수료를 후원하려면 `PAYMASTER_URL` 환경 변수를 설정하세요. 이는 백엔드가 모든 가스 비용을 부담하는 원활한 경험을 만들어 줍니다. [CDP Portal](https://portal.cdp.coinbase.com/)에서 페이마스터 URL을 받으세요.
</Tip>

### 자금 관리

기본적으로 청구된 USDC는 구독 소유자 지갑에 남아 있습니다. 선택적으로 `recipient` 주소를 지정하여 자금을 다른 주소로 자동 이체할 수 있습니다:

<Tabs>
  <Tab title="기본값 (소유자 지갑 보관)">
    ```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="트레저리 지갑으로 전송">
    ```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="동적 수신자">
    ```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>

### 테스트넷 테스트

라이브 배포 전에 Base Sepolia에서 구독 구현을 테스트하세요:

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

## 네트워크 및 토큰 지원

**Base 구독 (Base의 USDC):**

| 네트워크         | 체인 ID | 토큰   | 상태           |
| ------------ | ----- | ---- | ------------ |
| Base Mainnet | 8453  | USDC | ✅ 프로덕션 준비 완료 |
| Base Sepolia | 84532 | USDC | ✅ 테스트 가능     |

<Note>
  **커스텀 구현 가능**: Base 구독은 Base의 USDC에 최적화되어 있지만, 기반이 되는 [스펜드 퍼미션](/base-account/improve-ux/spend-permissions) 프리미티브를 사용하여 모든 EVM 호환 체인에서 모든 ERC-20 토큰 또는 네이티브 ETH로 커스텀 구독 구현을 구축할 수 있습니다.
</Note>

## 고급 주제

### 커스텀 트랜잭션 처리

트랜잭션 실행에 대한 수동 제어가 필요하거나 기존 지갑 인프라와 통합하려는 개발자를 위해 하위 수준의 유틸리티를 사용할 수 있습니다:

<AccordionGroup>
  <Accordion title="prepareCharge - 수동 청구 실행">
    CDP 지갑을 사용할 수 없는 경우, `prepareCharge()`는 수동으로 실행할 수 있는 호출 데이터를 제공합니다:

    ```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)
    ```

    자세한 내용은 이 섹션의 `prepareCharge()` 예시를 참고하세요.
  </Accordion>

  <Accordion title="prepareRevoke - 수동 취소 실행">
    마찬가지로, `prepareRevoke()`는 취소 호출 데이터를 제공합니다:

    ```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
    ```

    자세한 내용은 이 섹션의 `prepareRevoke()` 예시를 참고하세요.
  </Accordion>
</AccordionGroup>

## 핵심 함수 요약

* `subscribe()` - 프론트엔드에서 구독 생성
* `getStatus()` - 구독 상태 확인
* `charge()` - 백엔드에서 구독 청구
* `revoke()` - 백엔드에서 구독 취소
* `getOrCreateSubscriptionOwnerWallet()` - 구독 관리를 위한 CDP 소유자 지갑 설정
* `prepareCharge()` - 고급: 커스텀 청구 실행
* `prepareRevoke()` - 고급: 커스텀 취소 실행
* [스펜드 퍼미션](/base-account/improve-ux/spend-permissions) - 기반 프리미티브 심층 분석
* [일회성 결제](/base-account/guides/accept-payments) - 단일 결제 수락

<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>
)}
/>
