UNPKG

onramp-docs-cli

Version:

CLI tool to set up Onramp documentation and integration in your project

737 lines (563 loc) 23.4 kB
s# Add Coinbase Onramp to Existing Next.js App **Purpose:** Comprehensive instructions for integrating [Coinbase Developer Platform (CDP) Onramp](https://docs.cdp.coinbase.com/onramp-offramp/docs) into an existing Next.js (App Router) application using local documentation and best practices. **Scope:** All AI-generated advice or code related to CDP Onramp must follow these guardrails and utilize the provided documentation. --- ## **STEP 1: Install Documentation Package** **IMPORTANT:** Before starting the integration, install the onramp documentation package to access comprehensive guides and examples: ```bash npx -p onramp-docs-cli onramp-docs setup ``` Or use the interactive setup: ```bash npx -p onramp-docs-cli onramp-setup ``` This will create `doc/onramp/` in your project with the following documentation: - `onramp/intro.md` - API authentication and CDP setup - `onramp/onramp-overview.md` - Introduction and supported countries - `onramp/generating-onramp-url.md` - URL generation and session tokens - `onramp/one-click-buy-url.md` - Pre-filled buy experience - `onramp/generating-quotes.md` - Getting price quotes - `onramp/countries-and-currencies.md` - Supported regions and payment methods - `onramp/secure-init.md` - **MANDATORY** secure initialization requirements - `onramp/create-session-token.md` - Session token API details - `onramp/transaction-status.md` - Transaction monitoring **CRITICAL:** Reference these documentation files throughout the integration process for specific implementation details, troubleshooting, and best practices. --- ## **🚨 CRITICAL INSTRUCTIONS FOR AI CODING AGENTS 🚨** ### **PRE-IMPLEMENTATION REQUIREMENTS** **BEFORE** writing any code, you **MUST** read these documentation files in order: 1. **READ FIRST**: `doc/onramp/onramp/secure-init.md` - **Learn**: Session token mandatory migration (7/31/2025) - **Learn**: Session token properties (5-minute expiration, single-use) - **Learn**: Portal configuration toggle requirement 2. **READ SECOND**: `doc/onramp/onramp/intro.md` - **Learn**: JWT authentication setup for server-side operations - **Learn**: Secret vs Client API key differences - **Learn**: Environment variable setup patterns 3. **READ THIRD**: `doc/onramp/onramp/countries-and-currencies.md` - **Learn**: Country/state validation requirements (US states have asset restrictions) - **Learn**: Payment method limits by region - **Learn**: Required config and options API calls ### **ABSOLUTE REQUIREMENTS FOR NEXT.JS** You **MUST** follow these requirements or the implementation will **BREAK**: 1. **MANDATORY Session Token Usage** - See `doc/onramp/onramp/secure-init.md` for complete details: ```tsx // ❌ WRONG - Will fail after 7/31/2025 const url = `https://pay.coinbase.com/buy/select-asset?appId=${appId}&addresses=${addresses}`; // ✅ CORRECT - Uses session token (see secure-init.md) const sessionToken = await getSessionToken(); const url = `https://pay.coinbase.com/buy/select-asset?sessionToken=${sessionToken}`; ``` 2. **NEVER expose Secret API Keys on client** - Only in server actions/API routes (see `doc/onramp/onramp/intro.md`): ```tsx // ❌ WRONG - Secret key exposed on client "use client"; const SECRET_KEY = process.env.CDP_SECRET_KEY; // SECURITY VIOLATION // ✅ CORRECT - Server-side only (see intro.md JWT section) export async function getSessionToken() { 'use server'; const jwt = await generateJWT(); // Server-side operation } ``` 3. **ALWAYS validate user location FIRST** - See `doc/onramp/onramp/countries-and-currencies.md`: - US users MUST provide state (NY has asset restrictions) - Check supported countries via config API - Validate payment limits per region 4. **ALWAYS add `"use client"` directive** for browser APIs/React hooks 5. **ALWAYS use App Router structure** (`app/` directory, not `pages/`) ### **CRITICAL ERROR PATTERNS TO AVOID** Reference `doc/onramp/onramp/` files to avoid these failures: ```tsx // ❌ MISSING USER LOCATION VALIDATION - Will fail for restricted regions // Must read countries-and-currencies.md first // ❌ REUSING SESSION TOKENS - Single-use only (secure-init.md) const token = await getSessionToken(); window.open(`...?sessionToken=${token}`); // First use window.open(`...?sessionToken=${token}`); // WILL FAIL - token already used // ❌ NO PAYMENT LIMIT VALIDATION - Different limits per country/method // Must check countries-and-currencies.md for min/max amounts // ❌ MISSING GUEST CHECKOUT HANDLING - See onramp-overview.md // Users without Coinbase accounts → guest flow (different limits/methods) ``` --- ## **STEP 2: Choose Integration Approach** **FIRST**: Read `doc/onramp/onramp/onramp-overview.md` to understand supported countries and user flows. Select ONE approach based on your UX requirements: ### **Option A: Progressive Enhancement (Recommended)** 1. **Start with country validation** (see `doc/onramp/onramp/countries-and-currencies.md`) 2. **Show appropriate payment methods** per user's location 3. **Generate session tokens** with location-appropriate assets - **Best for**: Production apps with global users - **Docs to read**: `countries-and-currencies.md` → `generating-onramp-url.md` ### **Option B: One-Click-Buy (Best UX for Known Intent)** - **When to use**: User already selected asset + amount in your app - **Requirements**: Must validate payment limits per user's country first - **Docs to read**: `one-click-buy-url.md` + `generating-quotes.md` - **Critical**: Read quote limitations in `generating-quotes.md` ### **Option C: OnchainKit Components (Rapid Development)** - **Built-in handling**: Country validation, session tokens, error states - **Limitation**: Less customization control - **Still required**: User location detection for asset filtering - **Integration docs**: OnchainKit + `doc/onramp/onramp/generating-onramp-url.md` --- ## **STEP 3: Installation and Setup** ### **Install Required Packages** ```bash # Core packages for JWT and API calls npm install jsonwebtoken @types/jsonwebtoken # For OnchainKit integration (Option C only) npm install @coinbase/onchainkit # For user location detection (REQUIRED for all options) npm install @vercel/edge # or your preferred geo detection ``` ### **Environment Configuration** Create or update your `.env.local` file: ```bash # Project ID from https://portal.cdp.coinbase.com NEXT_PUBLIC_CDP_PROJECT_ID=your-project-id-here # Secret API Key (server-side only - see intro.md for creation) CDP_SECRET_KEY=your-secret-api-key-here CDP_SECRET_KEY_NAME=organizations/your-org/apiKeys/your-key-id ``` ### **Portal Configuration (MANDATORY)** **BEFORE any code works**, configure your project: 1. **Create Secret API Key**: Follow `doc/onramp/onramp/intro.md` setup instructions 2. **Enable Secure Init**: Read `doc/onramp/onramp/secure-init.md` Step 5 - toggle "Enforce secure initialization" in Portal 3. **Save API key securely** (never commit to version control) **Reference**: `doc/onramp/onramp/secure-init.md` for complete portal setup requirements. --- ## **STEP 4: Server-Side Setup** ### **Create JWT Authentication Utility** **IMPORTANT:** Create this utility for server-side JWT generation: ```tsx // lib/auth.ts import { sign } from 'jsonwebtoken'; import crypto from 'crypto'; export async function generateJWT(method = 'POST', host = 'api.developer.coinbase.com', path = '/onramp/v1/token') { const keyName = process.env.CDP_SECRET_KEY_NAME!; const keySecret = process.env.CDP_SECRET_KEY!; const uri = `${method} ${host}${path}`; const token = sign( { iss: 'cdp', nbf: Math.floor(Date.now() / 1000), exp: Math.floor(Date.now() / 1000) + 120, // JWT expires in 120 seconds sub: keyName, uri, }, keySecret, { algorithm: 'ES256', header: { kid: keyName, nonce: crypto.randomBytes(16).toString('hex'), }, } ); return token; } ``` ### **Create Session Token API Route** ```tsx // app/api/onramp/session-token/route.ts import { generateJWT } from '@/lib/auth'; import { NextRequest, NextResponse } from 'next/server'; export async function POST(request: NextRequest) { try { const { addresses, assets } = await request.json(); // Generate JWT for authentication const jwt = await generateJWT(); // Call CDP Session Token API const response = await fetch('https://api.developer.coinbase.com/onramp/v1/token', { method: 'POST', headers: { 'Authorization': `Bearer ${jwt}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ addresses, assets: assets || ['ETH', 'USDC', 'BTC'], }), }); if (!response.ok) { throw new Error(`Session token API failed: ${response.status}`); } const data = await response.json(); return NextResponse.json({ token: data.data.token, channelId: data.data.channel_id }); } catch (error) { console.error('Session token generation failed:', error); return NextResponse.json( { error: 'Failed to generate session token' }, { status: 500 } ); } } ``` --- ## **STEP 5: Client-Side Integration** ### **Option A: Direct URL Integration** **IMPORTANT:** Create a client component for onramp functionality: ```tsx // components/OnrampButton.tsx "use client"; import { useState } from 'react'; interface OnrampButtonProps { walletAddress: string; className?: string; children?: React.ReactNode; } export function OnrampButton({ walletAddress, className = '', children = 'Buy Crypto' }: OnrampButtonProps) { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState<string | null>(null); const handleOnramp = async () => { if (!walletAddress) { setError('Wallet address is required'); return; } setIsLoading(true); setError(null); try { // Generate session token via API route const response = await fetch('/api/onramp/session-token', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ addresses: [ { address: walletAddress, blockchains: ['base', 'ethereum'] } ], assets: ['ETH', 'USDC', 'BTC'] }), }); if (!response.ok) { throw new Error('Failed to generate session token'); } const { token } = await response.json(); // Construct onramp URL with session token const onrampUrl = new URL('https://pay.coinbase.com/buy/select-asset'); onrampUrl.searchParams.set('sessionToken', token); // Optional: Add default parameters for better UX onrampUrl.searchParams.set('defaultAsset', 'USDC'); onrampUrl.searchParams.set('defaultNetwork', 'base'); // Redirect to onramp window.open(onrampUrl.toString(), '_blank'); } catch (error) { console.error('Onramp error:', error); setError(error instanceof Error ? error.message : 'Failed to open onramp'); } finally { setIsLoading(false); } }; return ( <div> <button onClick={handleOnramp} disabled={isLoading || !walletAddress} className={`px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 ${className}`} > {isLoading ? 'Opening...' : children} </button> {error && ( <p className="mt-2 text-sm text-red-600">{error}</p> )} </div> ); } ``` ### **Option B: OnchainKit Integration** ```tsx // components/OnchainKitFund.tsx "use client"; import { FundButton, FundCard } from '@coinbase/onchainkit/fund'; import { base } from 'viem/chains'; interface OnchainKitFundProps { walletAddress: string; } export function OnchainKitFund({ walletAddress }: OnchainKitFundProps) { return ( <div className="space-y-4"> {/* Option 1: Fund Button */} <FundButton fundingUrl={`https://pay.coinbase.com/buy/select-asset?appId=${process.env.NEXT_PUBLIC_CDP_PROJECT_ID}&addresses=[{"address":"${walletAddress}","blockchains":["base"]}]`} /> {/* Option 2: Fund Card (full UI) */} <FundCard fundingUrl={`https://pay.coinbase.com/buy/select-asset?appId=${process.env.NEXT_PUBLIC_CDP_PROJECT_ID}&addresses=[{"address":"${walletAddress}","blockchains":["base"]}]`} className="w-full max-w-md" /> </div> ); } ``` ### **Option C: One-Click-Buy Implementation** ```tsx // components/OneClickBuy.tsx "use client"; import { useState } from 'react'; interface OneClickBuyProps { walletAddress: string; asset?: string; amount?: number; currency?: string; } export function OneClickBuy({ walletAddress, asset = 'USDC', amount = 10, currency = 'USD' }: OneClickBuyProps) { const [isLoading, setIsLoading] = useState(false); const handleOneClickBuy = async () => { setIsLoading(true); try { // Generate session token const response = await fetch('/api/onramp/session-token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ addresses: [{ address: walletAddress, blockchains: ['base'] }], assets: [asset] }), }); const { token } = await response.json(); // Create one-click-buy URL const onrampUrl = new URL('https://pay.coinbase.com/buy/select-asset'); onrampUrl.searchParams.set('sessionToken', token); onrampUrl.searchParams.set('defaultAsset', asset); onrampUrl.searchParams.set('presetFiatAmount', amount.toString()); onrampUrl.searchParams.set('fiatCurrency', currency); onrampUrl.searchParams.set('defaultPaymentMethod', 'CARD'); window.open(onrampUrl.toString(), '_blank'); } catch (error) { console.error('One-click buy failed:', error); } finally { setIsLoading(false); } }; return ( <button onClick={handleOneClickBuy} disabled={isLoading} className="px-6 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50" > {isLoading ? 'Loading...' : `Buy ${amount} ${currency} of ${asset}`} </button> ); } ``` --- ## **STEP 6: Integration into Your App** ### **Add to Your Existing Components** ```tsx // app/wallet/page.tsx or your existing wallet component import { OnrampButton } from '@/components/OnrampButton'; import { OneClickBuy } from '@/components/OneClickBuy'; export default function WalletPage() { // Assume you have wallet address from your existing wallet integration const walletAddress = "0x1234..."; // Your user's wallet address return ( <div className="space-y-6"> <h1>Your Wallet</h1> {/* Basic onramp integration */} <div> <h2>Fund Your Wallet</h2> <OnrampButton walletAddress={walletAddress} /> </div> {/* One-click buy options */} <div> <h2>Quick Buy</h2> <div className="space-y-2"> <OneClickBuy walletAddress={walletAddress} asset="USDC" amount={10} currency="USD" /> <OneClickBuy walletAddress={walletAddress} asset="ETH" amount={50} currency="USD" /> </div> </div> </div> ); } ``` --- ## **STEP 7: Advanced Features** ### **Transaction Status Monitoring** For transaction status updates, reference `doc/onramp/onramp/transaction-status.md`: ```tsx // lib/onramp-status.ts import { generateJWT } from './auth'; export async function getTransactionStatus(transactionId: string) { const jwt = await generateJWT('GET', 'api.developer.coinbase.com', `/onramp/v1/transactions/${transactionId}`); const response = await fetch(`https://api.developer.coinbase.com/onramp/v1/transactions/${transactionId}`, { headers: { 'Authorization': `Bearer ${jwt}`, }, }); return response.json(); } ``` ### **Quote Generation** For getting price quotes before purchase, see `doc/onramp/onramp/generating-quotes.md`: ```tsx // app/api/onramp/quote/route.ts import { generateJWT } from '@/lib/auth'; import { NextRequest, NextResponse } from 'next/server'; export async function POST(request: NextRequest) { try { const quoteParams = await request.json(); const jwt = await generateJWT('POST', 'api.developer.coinbase.com', '/onramp/v1/buy/quote'); const response = await fetch('https://api.developer.coinbase.com/onramp/v1/buy/quote', { method: 'POST', headers: { 'Authorization': `Bearer ${jwt}`, 'Content-Type': 'application/json', }, body: JSON.stringify(quoteParams), }); const data = await response.json(); return NextResponse.json(data); } catch (error) { return NextResponse.json({ error: 'Quote generation failed' }, { status: 500 }); } } ``` --- ## **STEP 8: Testing and Verification** ### **Development Testing** 1. **Start your development server:** ```bash npm run dev ``` 2. **Test the integration:** - Click the onramp button - Verify session token generation - Test the onramp flow in sandbox mode - Verify wallet address is correctly passed 3. **Check console for errors** - Monitor both client and server logs 4. **Test different scenarios:** - Different wallet addresses - Various asset types - Different amount presets ### **Common Issues and Solutions** Reference `doc/onramp/onramp/` documentation for troubleshooting: 1. **Session Token Errors:** Check `intro.md` for authentication setup 2. **Missing Wallet Address:** Ensure wallet is connected before showing onramp 3. **CORS Issues:** Verify domain is whitelisted in CDP dashboard 4. **Rate Limiting:** Implement proper error handling for 429 responses --- ## **CRITICAL LOCATION VALIDATION PATTERN** **MANDATORY:** Always validate user location before showing onramp (will fail without this): ```tsx // components/LocationAwareOnramp.tsx "use client"; import { useState, useEffect } from 'react'; export function LocationAwareOnramp({ walletAddress }: { walletAddress: string }) { const [locationData, setLocationData] = useState<any>(null); const [isSupported, setIsSupported] = useState<boolean | null>(null); useEffect(() => { async function checkLocation() { try { // Get user location (implement your geo detection) const location = await getUserLocation(); // { country: 'US', subdivision: 'NY' } // Validate support via your API route const response = await fetch(`/api/onramp/validate-location?country=${location.country}&subdivision=${location.subdivision}`); const data = await response.json(); setLocationData(location); setIsSupported(data.supported); } catch (error) { console.error('Location check failed:', error); setIsSupported(false); } } checkLocation(); }, []); if (isSupported === null) return <div>Checking availability...</div>; if (!isSupported) return <div>Onramp not available in your region</div>; // Show onramp components only after location validation return <OnrampButton walletAddress={walletAddress} location={locationData} />; } ``` ## **PRODUCTION ERROR HANDLING PATTERNS** **CRITICAL:** Handle these failure scenarios (see relevant doc files): ```tsx // Essential error handling for production export async function handleOnrampErrors(error: any, context: string) { // Rate limiting (see onramp-overview.md - 10 req/sec limit) if (error.status === 429) { throw new Error('Rate limited. Please try again in a few seconds.'); } // Session token expired/reused (see secure-init.md - 5min expiry, single-use) if (error.message?.includes('session_token')) { throw new Error('Session expired. Please refresh and try again.'); } // Country/payment restrictions (see countries-and-currencies.md) if (error.message?.includes('not_supported')) { throw new Error('This payment method is not available in your region.'); } // Payment amount limits (see countries-and-currencies.md) if (error.message?.includes('amount')) { throw new Error('Amount outside allowed limits for your region.'); } // Generic fallback console.error(`Onramp error in ${context}:`, error); throw new Error('Onramp service temporarily unavailable.'); } ``` ## **AI MODEL VERIFICATION STEPS** Before generating any CDP Onramp code, you **MUST** verify: 1. **Pre-Implementation Reading**: Did you read the required docs in PRE-IMPLEMENTATION order? 2. **Location Validation**: Are you checking user location BEFORE showing onramp? 3. **Session Token Properties**: Are you handling 5-minute expiry and single-use correctly? 4. **Payment Limits**: Are you validating amounts against country/payment method limits? 5. **Guest Checkout**: Are you handling users without Coinbase accounts ($500/week limit)? 6. **Error Handling**: Did you implement production error patterns above? 7. **Rate Limiting**: Are you handling 10 req/sec API limits? 8. **Documentation Reference**: Did you reference specific doc files for each feature? 9. **Client Directive**: Does EVERY client-side file start with `"use client";`? 10. **Server-Side Security**: Are Secret API Keys only used in server actions/API routes? If ANY check **fails**, **STOP** and revise until compliance is achieved. --- ## **DEPLOYMENT CHECKLIST** Before deploying to production: - [ ] **Location validation implemented** (CRITICAL - will fail without this) - [ ] **Session tokens implemented** (mandatory after 7/31/2025) - [ ] **Portal secure init toggle enabled** (see secure-init.md Step 5) - [ ] **Error handling for all failure scenarios** (rate limiting, expired tokens, etc.) - [ ] **User location detection** (country + US state validation) - [ ] **Payment limit validation** per region - [ ] **Guest checkout flow tested** ($500/week limit messaging) - [ ] Environment variables set in deployment platform - [ ] Secret API keys secured (never exposed on client) - [ ] HTTPS enabled - [ ] Loading states added - [ ] Wallet connection verified - [ ] Domain whitelisted in CDP dashboard - [ ] Rate limiting handling implemented (10 req/sec) - [ ] Documentation reviewed for production considerations --- ## **SUPPORT AND DOCUMENTATION** - **Local Documentation:** `doc/onramp/onramp/` (installed via setup command) - **CDP Portal:** https://portal.cdp.coinbase.com - **Official Docs:** https://docs.cdp.coinbase.com/onramp-offramp/docs - **OnchainKit:** https://onchainkit.xyz/fund/fund-card - **Discord Support:** https://discord.com/invite/cdp (join #onramp channel) **Remember:** Always reference the local documentation first, as it contains the most relevant patterns and examples for your specific use case. --- **FINAL NOTE:** This integration enables your users to seamlessly purchase cryptocurrency and have it sent directly to their wallet address. The onramp handles all payment processing, KYC compliance, and cryptocurrency delivery, while giving you control over the user experience and integration points.