Skip to main content

Documentation Index

Fetch the complete documentation index at: https://daehan-base.mintlify.app/llms.txt

Use this file to discover all available pages before exploring further.

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

Base 구독을 통해 자동 USDC 결제를 수락하여 예측 가능한 반복 수익 흐름을 구축할 수 있습니다. SaaS 플랫폼, 콘텐츠 구독 서비스, 또는 정기 결제가 필요한 비즈니스 모델을 운영하든 관계없이, Base 구독은 가맹점 수수료 없이 원활한 솔루션을 제공합니다. 주요 기능:
비즈니스 모델에 맞는 모든 청구 주기를 지원합니다:
  • 단기 서비스를 위한 일별 구독
  • 정기 배달 또는 서비스를 위한 주별
  • 표준 SaaS 구독을 위한 월별
  • 할인된 장기 약정을 위한 연간
  • 고유한 모델을 위한 커스텀 기간 (예: 14일, 90일)
허용된 한도 내에서 모든 금액을 청구할 수 있습니다:
  • 예측 가능한 청구를 위한 고정 반복 금액
  • 한도 내 가변 사용량 기반 요금
  • 다양한 청구 금액을 가진 계층형 요금제
  • 주기 중간 변경을 위한 비례 배분 요금
구독 생명주기에 대한 완전한 제어:
  • 활성 구독 확인을 위한 실시간 상태 확인
  • 현재 기간의 남은 청구 금액
  • 계획 수립을 위한 다음 기간 시작일
  • 즉각적인 업데이트를 위한 취소 감지
프로덕션 사용 사례를 위해 설계됨:
  • 트랜잭션 수수료 또는 플랫폼 수수료 없음
  • USDC 스테이블코인으로 즉시 정산
  • 개발 및 테스트를 위한 테스트넷 지원
  • 회계를 위한 상세 트랜잭션 내역
  • SDK를 통한 프로그래밍 방식 접근

작동 방식

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

사용자가 구독 승인

고객이 매 청구 기간마다 지정된 금액까지 지갑을 청구할 수 있는 권한을 애플리케이션에 부여합니다. 이는 취소될 때까지 유지되는 일회성 승인입니다.
2

애플리케이션이 주기적으로 청구

백엔드 서비스가 사용자 상호작용 없이 결제 기한이 됐을 때 구독을 청구합니다. 기간당 승인된 금액까지 청구할 수 있습니다.
3

스마트 기간 관리

지출 한도는 각 새 기간 시작 시 자동으로 초기화됩니다. 한 기간에 전액을 청구하지 않아도 이월되지 않습니다.
4

사용자가 제어권 유지

고객은 언제든지 지갑을 통해 구독을 확인하고 취소할 수 있어 투명성과 신뢰를 보장합니다.

구현 가이드

아키텍처 개요

완전한 구독 구현에는 클라이언트와 서버 컴포넌트가 모두 필요합니다: 클라이언트 측 (프론트엔드):
  • 구독 생성을 위한 사용자 인터페이스
  • 지갑 요청 생성 및 사용자 응답 처리
서버 측 (백엔드 - Node.js):
  • 청구 및 취소 실행을 위한 CDP 스마트 지갑
  • 주기적 청구를 위한 스케줄링 작업
  • 구독 추적을 위한 데이터베이스
  • 상태 업데이트 핸들러
  • 실패한 청구를 위한 재시도 로직
CDP 기반 백엔드Base 구독은 손쉬운 백엔드 관리를 위해 CDP(Coinbase Developer Platform) 서버 지갑을 사용합니다. charge()revoke() 함수가 모든 트랜잭션 세부 사항을 자동으로 처리합니다:
  • ✅ 자동 지갑 관리
  • ✅ 내장된 트랜잭션 서명
  • ✅ 가스 추정 및 논스 처리
  • ✅ 가스리스 트랜잭션을 위한 선택적 페이마스터 지원
CDP Portal에서 CDP 자격 증명을 받으세요.
보안 요구 사항정기 결제를 수락하려면 다음이 필요합니다:
  1. CDP 자격 증명 (API 키 ID, 시크릿, 지갑 시크릿)
  2. 청구를 안전하게 실행하기 위한 백엔드 인프라 (Node.js)
  3. 구독 ID 저장 및 관리를 위한 데이터베이스
  4. 클라이언트 측 코드에 CDP 자격 증명을 절대 노출하지 마세요

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

먼저 구독 소유자 역할을 할 CDP 스마트 지갑을 설정하세요:
backend/setup.ts
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;
}
백엔드 전용: 이 설정은 CDP 자격 증명을 가진 Node.js 백엔드에서 실행됩니다. 결과 지갑 주소는 공개적이며 subscribe() 호출 시 사용하기 위해 프론트엔드에 안전하게 공유할 수 있습니다.
CDP 자격 증명을 비공개로 유지하세요: CDP 자격 증명(API 키, 시크릿)을 프론트엔드에 절대 노출하지 마세요. 구독 소유자 지갑 주소만 프론트엔드에서 접근 가능해야 합니다.

클라이언트 측: 구독 생성

사용자는 프론트엔드 애플리케이션에서 구독을 생성합니다:
SubscriptionButton.tsx
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를 사용하여 백엔드에서 손쉽게 청구를 실행합니다:
chargeSubscriptions.ts
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 };
  }
}

서버 측: 구독 취소

백엔드에서 프로그래밍 방식으로 구독을 취소합니다:
revokeSubscription.ts
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');
}
자동 트랜잭션 관리: charge()revoke() 함수는 지갑 관리, 가스 추정, 논스 처리, 트랜잭션 확인을 포함한 모든 트랜잭션 세부 사항을 처리합니다. 사용자를 위한 가스리스 트랜잭션을 활성화하려면 paymasterUrl 파라미터를 사용하세요.
가스리스 트랜잭션: 구독 청구 및 취소에 대한 가스 수수료를 후원하려면 PAYMASTER_URL 환경 변수를 설정하세요. 이는 백엔드가 모든 가스 비용을 부담하는 원활한 경험을 만들어 줍니다. CDP Portal에서 페이마스터 URL을 받으세요.

자금 관리

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

테스트넷 테스트

라이브 배포 전에 Base Sepolia에서 구독 구현을 테스트하세요:
testnet-frontend.ts
// 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
});
testnet-backend.ts
// 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 Mainnet8453USDC✅ 프로덕션 준비 완료
Base Sepolia84532USDC✅ 테스트 가능
커스텀 구현 가능: Base 구독은 Base의 USDC에 최적화되어 있지만, 기반이 되는 스펜드 퍼미션 프리미티브를 사용하여 모든 EVM 호환 체인에서 모든 ERC-20 토큰 또는 네이티브 ETH로 커스텀 구독 구현을 구축할 수 있습니다.

고급 주제

커스텀 트랜잭션 처리

트랜잭션 실행에 대한 수동 제어가 필요하거나 기존 지갑 인프라와 통합하려는 개발자를 위해 하위 수준의 유틸리티를 사용할 수 있습니다:
CDP 지갑을 사용할 수 없는 경우, prepareCharge()는 수동으로 실행할 수 있는 호출 데이터를 제공합니다:
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() 예시를 참고하세요.
마찬가지로, prepareRevoke()는 취소 호출 데이터를 제공합니다:
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() 예시를 참고하세요.

핵심 함수 요약

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