diff --git a/README.md b/README.md index d3d19bbb..9a87db19 100644 --- a/README.md +++ b/README.md @@ -1,166 +1,1802 @@ -

-Base logo -

+# fetchOnrampOptions - +## Overview -[![GitHub contributors](https://img.shields.io/github/contributors/base/docs)](https://github.com/base/docs/graphs/contributors) -[![GitHub commit activity](https://img.shields.io/github/commit-activity/w/base/docs)](https://github.com/base/docs/graphs/contributors) -[![GitHub Stars](https://img.shields.io/github/stars/base/docs.svg)](https://github.com/base/docs/stargazers) -![GitHub repo size](https://img.shields.io/github/repo-size/base/docs) -[![GitHub](https://img.shields.io/github/license/base/docs?color=blue)](https://github.com/base/docs/blob/main/LICENSE.md) +The `fetchOnrampOptions` utility is a powerful function that retrieves jurisdiction-specific onramp data, including supported fiat currencies and available cryptocurrency assets. This utility is essential for building compliant onramp experiences that respect regional regulatory requirements and asset availability. - +## Table of Contents +- [Overview](#overview) +- [What is fetchOnrampOptions?](#what-is-fetchonrampoptions) +- [Use Cases](#use-cases) +- [Prerequisites](#prerequisites) +- [Basic Usage](#basic-usage) +- [Parameters](#parameters) +- [Return Value](#return-value) +- [Advanced Usage](#advanced-usage) +- [Best Practices](#best-practices) +- [Error Handling](#error-handling) +- [Country-Specific Guidelines](#country-specific-guidelines) +- [Troubleshooting](#troubleshooting) +- [Related Resources](#related-resources) -[![Website base.org](https://img.shields.io/website-up-down-green-red/https/base.org.svg)](https://base.org) -[![Blog](https://img.shields.io/badge/blog-up-green)](https://base.mirror.xyz/) -[![Docs](https://img.shields.io/badge/docs-up-green)](https://docs.base.org/) -[![Discord](https://img.shields.io/discord/1067165013397213286?label=discord)](https://base.org/discord) -[![Twitter Base](https://img.shields.io/twitter/follow/Base?style=social)](https://twitter.com/Base) +--- - +## What is fetchOnrampOptions? -[![GitHub pull requests by-label](https://img.shields.io/github/issues-pr-raw/base/docs)](https://github.com/base/docs/pulls) -[![GitHub Issues](https://img.shields.io/github/issues-raw/base/docs.svg)](https://github.com/base/docs/issues) +`fetchOnrampOptions` queries the Coinbase Onramp API to determine: -Base Docs are community-managed. We welcome and encourage contributions from everyone to keep these docs accurate, helpful, and up to date. +1. **Available Fiat Currencies** - Which fiat currencies can be used for purchases (e.g., USD, EUR, GBP) +2. **Supported Crypto Assets** - Which cryptocurrencies can be purchased in the specified jurisdiction +3. **Network Availability** - Which blockchain networks are available for each asset +4. **Regional Restrictions** - Jurisdiction-specific limitations and requirements -> Note: This repository powers the public Base documentation site. Content lives under `docs/`. +This information enables you to: +- Build compliant onramp flows that only show available options +- Provide accurate asset selection based on user location +- Handle regional variations in supported assets and currencies +- Create seamless user experiences with pre-filtered options -## Local development +--- -Prerequisite: Node.js v19+. +## Use Cases -1. Clone the repository. -2. Install the Mint CLI to preview documentation changes locally: +### 1. Dynamic Asset Filtering +Filter available cryptocurrencies based on user location to ensure compliance: -```bash -npm i -g mint +```typescript +import { fetchOnrampOptions } from '@coinbase/onchainkit/fund'; + +async function getAvailableAssets(country: string, subdivision?: string) { + const options = await fetchOnrampOptions({ + country, + subdivision, + apiKey: process.env.ONRAMP_API_KEY + }); + + return options.assets.map(asset => ({ + id: asset.id, + name: asset.name, + availableNetworks: asset.networks.length + })); +} ``` -3. Preview locally (run from the `docs/` directory where `docs.json` lives): +### 2. Currency Selection UI +Build a currency selector that only shows available options: -```bash -cd docs -mint dev +```tsx +import { fetchOnrampOptions } from '@coinbase/onchainkit/fund'; +import { useState, useEffect } from 'react'; + +function CurrencySelector({ country, subdivision }) { + const [currencies, setCurrencies] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + async function loadCurrencies() { + try { + const options = await fetchOnrampOptions({ country, subdivision }); + setCurrencies(options.fiatCurrencies); + } catch (error) { + console.error('Failed to load currencies:', error); + } finally { + setLoading(false); + } + } + + loadCurrencies(); + }, [country, subdivision]); + + if (loading) return
Loading currencies...
; + + return ( + + ); +} ``` -Alternatively, without a global install: +### 3. Compliance Validation +Validate that a user's intended purchase is available in their region: + +```typescript +import { fetchOnrampOptions } from '@coinbase/onchainkit/fund'; + +async function validatePurchase( + country: string, + subdivision: string | undefined, + assetId: string, + fiatCurrencyId: string +): Promise<{ valid: boolean; reason?: string }> { + const options = await fetchOnrampOptions({ country, subdivision }); + + // Check if fiat currency is supported + const isFiatSupported = options.fiatCurrencies.some( + currency => currency.id === fiatCurrencyId + ); + + if (!isFiatSupported) { + return { + valid: false, + reason: `${fiatCurrencyId} is not supported in ${country}` + }; + } + + // Check if asset is available + const isAssetAvailable = options.assets.some( + asset => asset.id === assetId + ); + + if (!isAssetAvailable) { + return { + valid: false, + reason: `${assetId} is not available for purchase in ${country}` + }; + } + + return { valid: true }; +} +``` + +### 4. Network Selection +Display available networks for a specific asset: + +```typescript +import { fetchOnrampOptions } from '@coinbase/onchainkit/fund'; + +async function getAssetNetworks( + country: string, + assetId: string, + subdivision?: string +) { + const options = await fetchOnrampOptions({ country, subdivision }); + + const asset = options.assets.find(a => a.id === assetId); + + if (!asset) { + throw new Error(`Asset ${assetId} not available in ${country}`); + } + + return asset.networks.map(network => ({ + id: network.id, + name: network.name, + chainId: network.config.chainId + })); +} + +// Usage +const ethNetworks = await getAssetNetworks('US', 'ETH', 'CA'); +console.log(ethNetworks); +// [{ id: 'ethereum', name: 'Ethereum', chainId: '0x1' }, ...] +``` + +--- + +## Prerequisites +### API Key Requirements +You need a Coinbase Onramp API key to use this utility. There are two ways to provide it: + +1. **Via OnchainKitProvider** (React applications): + ```tsx + import { OnchainKitProvider } from '@coinbase/onchainkit'; + + function App() { + return ( + + {/* Your app */} + + ); + } + ``` + +2. **Direct Parameter** (non-React or outside provider): + ```typescript + const options = await fetchOnrampOptions({ + country: 'US', + subdivision: 'CA', + apiKey: 'your-api-key' + }); + ``` + +### Getting an API Key +1. Visit the [Coinbase Developer Platform](https://portal.cdp.coinbase.com) +2. Create a project or select an existing one +3. Navigate to the Onramp section +4. Generate or copy your API key + +### Dependencies ```bash -npx mint dev +npm install @coinbase/onchainkit +# or +yarn add @coinbase/onchainkit +# or +pnpm add @coinbase/onchainkit +``` + +--- + +## Basic Usage + +### Simple Example + +```typescript +import { fetchOnrampOptions } from '@coinbase/onchainkit/fund'; + +async function checkAvailability() { + const options = await fetchOnrampOptions({ + country: 'US', + subdivision: 'CA', // Required for US + apiKey: process.env.ONRAMP_API_KEY + }); + + console.log('Available currencies:', options.fiatCurrencies); + console.log('Available assets:', options.assets); +} + +checkAvailability(); +``` + +### React Component Example + +```tsx +import { fetchOnrampOptions } from '@coinbase/onchainkit/fund'; +import { useState, useEffect } from 'react'; + +function OnrampOptions({ country, subdivision }) { + const [options, setOptions] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + async function loadOptions() { + try { + const data = await fetchOnrampOptions({ + country, + subdivision + }); + setOptions(data); + } catch (err) { + setError(err.message); + } finally { + setLoading(false); + } + } + + loadOptions(); + }, [country, subdivision]); + + if (loading) return
Loading options...
; + if (error) return
Error: {error}
; + + return ( +
+

Available Currencies

+ + +

Available Assets

+ +
+ ); +} +``` + +### Next.js Server-Side Example + +```typescript +// app/api/onramp-options/route.ts +import { fetchOnrampOptions } from '@coinbase/onchainkit/fund'; +import { NextResponse } from 'next/server'; + +export async function GET(request: Request) { + const { searchParams } = new URL(request.url); + const country = searchParams.get('country'); + const subdivision = searchParams.get('subdivision'); + + if (!country) { + return NextResponse.json( + { error: 'Country parameter is required' }, + { status: 400 } + ); + } + + try { + const options = await fetchOnrampOptions({ + country, + subdivision: subdivision || undefined, + apiKey: process.env.ONRAMP_API_KEY + }); + + return NextResponse.json(options); + } catch (error) { + return NextResponse.json( + { error: 'Failed to fetch onramp options' }, + { status: 500 } + ); + } +} +``` + +--- + +## Parameters + +### Complete Parameter Reference + +```typescript +interface FetchOnrampOptionsParams { + country: string; + subdivision?: string; + apiKey?: string; +} +``` + +### `country` (required) + +**Type:** `string` + +**Description:** ISO 3166-1 alpha-2 two-letter country code representing the user's country of residence. + +**Format:** Uppercase two-letter code (e.g., `'US'`, `'GB'`, `'DE'`) + +**Examples:** +```typescript +// United States +fetchOnrampOptions({ country: 'US', subdivision: 'NY' }); + +// United Kingdom +fetchOnrampOptions({ country: 'GB' }); + +// Germany +fetchOnrampOptions({ country: 'DE' }); + +// Japan +fetchOnrampOptions({ country: 'JP' }); + +// Brazil +fetchOnrampOptions({ country: 'BR' }); +``` + +**Validation:** +- Must be a valid ISO 3166-1 alpha-2 code +- Must be uppercase +- Cannot be empty or null + +**Common Country Codes:** +| Country | Code | Subdivision Required | +|---------|------|---------------------| +| United States | US | Yes | +| United Kingdom | GB | No | +| Canada | CA | No | +| Germany | DE | No | +| France | FR | No | +| Japan | JP | No | +| Australia | AU | No | +| Brazil | BR | No | +| India | IN | No | +| Singapore | SG | No | + +### `subdivision` (conditionally required) + +**Type:** `string | undefined` + +**Description:** ISO 3166-2 two-letter subdivision code representing the user's state or province within their country. + +**Required When:** +- `country = 'US'` - Required for United States residents due to state-specific regulations + +**Format:** Uppercase two-letter code (e.g., `'NY'`, `'CA'`, `'TX'`) + +**Examples:** +```typescript +// US residents - subdivision required +fetchOnrampOptions({ country: 'US', subdivision: 'CA' }); // California +fetchOnrampOptions({ country: 'US', subdivision: 'NY' }); // New York +fetchOnrampOptions({ country: 'US', subdivision: 'TX' }); // Texas + +// Non-US residents - subdivision optional +fetchOnrampOptions({ country: 'GB' }); // UK - no subdivision needed +fetchOnrampOptions({ country: 'CA' }); // Canada - optional +``` + +**US State Codes Reference:** +| State | Code | State | Code | +|-------|------|-------|------| +| Alabama | AL | Montana | MT | +| Alaska | AK | Nebraska | NE | +| Arizona | AZ | Nevada | NV | +| Arkansas | AR | New Hampshire | NH | +| California | CA | New Jersey | NJ | +| Colorado | CO | New Mexico | NM | +| Connecticut | CT | New York | NY | +| Delaware | DE | North Carolina | NC | +| Florida | FL | North Dakota | ND | +| Georgia | GA | Ohio | OH | +| Hawaii | HI | Oklahoma | OK | +| Idaho | ID | Oregon | OR | +| Illinois | IL | Pennsylvania | PA | +| Indiana | IN | Rhode Island | RI | +| Iowa | IA | South Carolina | SC | +| Kansas | KS | South Dakota | SD | +| Kentucky | KY | Tennessee | TN | +| Louisiana | LA | Texas | TX | +| Maine | ME | Utah | UT | +| Maryland | MD | Vermont | VT | +| Massachusetts | MA | Virginia | VA | +| Michigan | MI | Washington | WA | +| Minnesota | MN | West Virginia | WV | +| Mississippi | MS | Wisconsin | WI | +| Missouri | MO | Wyoming | WY | + +**Why US Requires Subdivision:** +Certain US states have specific cryptocurrency regulations and restrictions. For example: +- New York requires a BitLicense for certain operations +- Some states have different asset availability +- Regulatory compliance varies by state + +### `apiKey` (conditionally required) + +**Type:** `string | undefined` + +**Description:** Your Coinbase Onramp API key for authentication. + +**Required When:** +- Using the utility outside of `OnchainKitProvider` context +- Using in a non-React environment (Node.js, serverless functions) +- Server-side rendering without provider context + +**Optional When:** +- Used within a React component tree wrapped by `OnchainKitProvider` +- The provider already has an API key configured + +**Examples:** +```typescript +// With explicit API key +const options = await fetchOnrampOptions({ + country: 'US', + subdivision: 'CA', + apiKey: 'your-api-key-here' +}); + +// With environment variable +const options = await fetchOnrampOptions({ + country: 'GB', + apiKey: process.env.COINBASE_ONRAMP_API_KEY +}); + +// In React with provider (no apiKey needed) +function MyComponent() { + const [options, setOptions] = useState(null); + + useEffect(() => { + fetchOnrampOptions({ country: 'US', subdivision: 'CA' }) + .then(setOptions); + }, []); + + // ... +} +``` + +**Security Best Practices:** +```typescript +// ✅ Good: Use environment variables +apiKey: process.env.COINBASE_ONRAMP_API_KEY + +// ✅ Good: Server-side only +// In Next.js API route or server component +apiKey: process.env.ONRAMP_API_KEY + +// ❌ Bad: Never hardcode in client-side code +apiKey: 'pk_live_123456789' // Don't do this! + +// ❌ Bad: Never commit to version control +apiKey: 'your-actual-key-here' // Don't do this! +``` + +--- + +## Return Value + +### Type Definition + +```typescript +interface OnrampOptionsResponseData { + fiatCurrencies: FiatCurrency[]; + assets: Asset[]; +} + +interface FiatCurrency { + id: string; // Currency code (e.g., 'USD') + name: string; // Full name (e.g., 'US Dollar') + symbol: string; // Currency symbol (e.g., '$') +} + +interface Asset { + id: string; // Asset symbol (e.g., 'ETH') + name: string; // Full name (e.g., 'Ethereum') + networks: Network[]; // Available networks +} + +interface Network { + id: string; // Network identifier (e.g., 'ethereum') + name: string; // Display name (e.g., 'Ethereum') + config: { + chainId: string; // Hex chain ID (e.g., '0x1') + }; +} +``` + +### Example Response + +```typescript +{ + fiatCurrencies: [ + { + id: "USD", + name: "US Dollar", + symbol: "$" + }, + { + id: "EUR", + name: "Euro", + symbol: "€" + } + ], + assets: [ + { + id: "ETH", + name: "Ethereum", + networks: [ + { + id: "ethereum", + name: "Ethereum", + config: { + chainId: "0x1" // Mainnet + } + }, + { + id: "base", + name: "Base", + config: { + chainId: "0x2105" // Base mainnet + } + } + ] + }, + { + id: "BTC", + name: "Bitcoin", + networks: [ + { + id: "bitcoin", + name: "Bitcoin", + config: { + chainId: "0x0" // Bitcoin doesn't use EVM chain IDs + } + } + ] + }, + { + id: "USDC", + name: "USD Coin", + networks: [ + { + id: "ethereum", + name: "Ethereum", + config: { + chainId: "0x1" + } + }, + { + id: "base", + name: "Base", + config: { + chainId: "0x2105" + } + }, + { + id: "polygon", + name: "Polygon", + config: { + chainId: "0x89" + } + } + ] + } + ] +} +``` + +### Understanding the Response + +#### Fiat Currencies Array +Each fiat currency object contains: +- **`id`**: The ISO 4217 currency code used in transactions +- **`name`**: Human-readable currency name for display +- **`symbol`**: The currency symbol for formatting amounts + +```typescript +// Using fiat currency data +options.fiatCurrencies.forEach(currency => { + console.log(`${currency.symbol} ${currency.name} (${currency.id})`); +}); +// Output: $ US Dollar (USD) +``` + +#### Assets Array +Each asset object contains: +- **`id`**: The asset symbol/ticker (e.g., ETH, BTC, USDC) +- **`name`**: Full asset name for display +- **`networks`**: Array of blockchain networks where this asset can be purchased + +```typescript +// Finding a specific asset +const eth = options.assets.find(asset => asset.id === 'ETH'); +console.log(`${eth.name} available on ${eth.networks.length} networks`); +``` + +#### Networks Array +Each network object contains: +- **`id`**: Network identifier for API calls +- **`name`**: Display name for the network +- **`config.chainId`**: Hexadecimal chain ID for wallet connections + +```typescript +// Converting chain ID to decimal +const chainIdHex = network.config.chainId; +const chainIdDecimal = parseInt(chainIdHex, 16); + +// Example: '0x1' -> 1 (Ethereum Mainnet) +// Example: '0x2105' -> 8453 (Base Mainnet) +``` + +--- + +## Advanced Usage + +### 1. Caching Strategy + +Implement caching to reduce API calls and improve performance: + +```typescript +import { fetchOnrampOptions } from '@coinbase/onchainkit/fund'; + +interface CacheEntry { + data: OnrampOptionsResponseData; + timestamp: number; +} + +class OnrampOptionsCache { + private cache: Map = new Map(); + private ttl: number; // Time to live in milliseconds + + constructor(ttlMinutes: number = 60) { + this.ttl = ttlMinutes * 60 * 1000; + } + + private getCacheKey(country: string, subdivision?: string): string { + return `${country}${subdivision ? `-${subdivision}` : ''}`; + } + + private isExpired(entry: CacheEntry): boolean { + return Date.now() - entry.timestamp > this.ttl; + } + + async getOptions( + country: string, + subdivision?: string + ): Promise { + const key = this.getCacheKey(country, subdivision); + const cached = this.cache.get(key); + + if (cached && !this.isExpired(cached)) { + console.log('Cache hit for', key); + return cached.data; + } + + console.log('Cache miss for', key); + const data = await fetchOnrampOptions({ + country, + subdivision, + apiKey: process.env.ONRAMP_API_KEY + }); + + this.cache.set(key, { + data, + timestamp: Date.now() + }); + + return data; + } + + clear(): void { + this.cache.clear(); + } + + invalidate(country: string, subdivision?: string): void { + const key = this.getCacheKey(country, subdivision); + this.cache.delete(key); + } +} + +// Usage +const cache = new OnrampOptionsCache(30); // 30-minute TTL + +async function getOptions(country: string, subdivision?: string) { + return cache.getOptions(country, subdivision); +} +``` + +### 2. Custom React Hook + +Create a reusable hook with built-in caching and error handling: + +```typescript +import { fetchOnrampOptions } from '@coinbase/onchainkit/fund'; +import { useState, useEffect, useCallback } from 'react'; + +interface UseOnrampOptionsResult { + options: OnrampOptionsResponseData | null; + loading: boolean; + error: Error | null; + refetch: () => Promise; +} + +function useOnrampOptions( + country: string, + subdivision?: string +): UseOnrampOptionsResult { + const [options, setOptions] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchOptions = useCallback(async () => { + setLoading(true); + setError(null); + + try { + const data = await fetchOnrampOptions({ + country, + subdivision + }); + setOptions(data); + } catch (err) { + setError(err instanceof Error ? err : new Error('Unknown error')); + } finally { + setLoading(false); + } + }, [country, subdivision]); + + useEffect(() => { + fetchOptions(); + }, [fetchOptions]); + + return { options, loading, error, refetch: fetchOptions }; +} + +// Usage +function AssetSelector({ country, subdivision }) { + const { options, loading, error, refetch } = useOnrampOptions( + country, + subdivision + ); + + if (loading) return
Loading assets...
; + if (error) { + return ( +
+

Error loading options: {error.message}

+ +
+ ); + } + + return ( + + ); +} +``` + +### 3. Location Detection Integration + +Combine with geolocation to automatically detect user location: + +```typescript +import { fetchOnrampOptions } from '@coinbase/onchainkit/fund'; + +interface LocationData { + country: string; + subdivision?: string; +} + +async function detectUserLocation(): Promise { + // Using a geolocation API service + const response = await fetch('https://ipapi.co/json/'); + const data = await response.json(); + + return { + country: data.country_code, + subdivision: data.region_code || undefined + }; +} + +async function getOptionsForUser(): Promise { + const location = await detectUserLocation(); + + console.log(`Detected location: ${location.country}`, location.subdivision); + + return fetchOnrampOptions({ + country: location.country, + subdivision: location.subdivision, + apiKey: process.env.ONRAMP_API_KEY + }); +} + +// React component example +function AutoDetectOnramp() { + const [location, setLocation] = useState(null); + const [options, setOptions] = useState(null); + + useEffect(() => { + async function setup() { + const userLocation = await detectUserLocation(); + setLocation(userLocation); + + const onrampOptions = await fetchOnrampOptions({ + country: userLocation.country, + subdivision: userLocation.subdivision + }); + setOptions(onrampOptions); + } + + setup(); + }, []); + + if (!location || !options) { + return
Detecting your location...
; + } + + return ( +
+

Your location: {location.country} {location.subdivision}

+ +
+ ); +} +``` + +### 4. Multi-Region Comparison + +Compare available options across different regions: + +```typescript +import { fetchOnrampOptions } from '@coinbase/onchainkit/fund'; + +interface RegionComparison { + country: string; + subdivision?: string; + fiatCurrencies: number; + assets: number; + uniqueAssets: string[]; +} + +async function compareRegions( + regions: Array<{ country: string; subdivision?: string }> +): Promise { + const comparisons = await Promise.all( + regions.map(async (region) => { + const options = await fetchOnrampOptions({ + ...region, + apiKey: process.env.ONRAMP_API_KEY + }); + + return { + ...region, + fiatCurrencies: options.fiatCurrencies.length, + assets: options.assets.length, + uniqueAssets: options.assets.map(a => a.id) + }; + }) + ); + + return comparisons; +} + +// Usage +const comparison = await compareRegions([ + { country: 'US', subdivision: 'NY' }, + { country: 'US', subdivision: 'CA' }, + { country: 'GB' }, + { country: 'DE' } +]); + +console.table(comparison); +``` + +### 5. Asset Availability Checker + +Create a utility to check if specific assets are available: + +```typescript +import { fetchOnrampOptions } from '@coinbase/onchainkit/fund'; + +interface AssetAvailability { + available: boolean; + networks?: Network[]; + reason?: string; +} + +async function checkAssetAvailability( + country: string, + assetId: string, + subdivision?: string +): Promise { + try { + const options = await fetchOnrampOptions({ + country, + subdivision, + apiKey: process.env.ONRAMP_API_KEY + }); + + const asset = options.assets.find(a => a.id === assetId); + + if (!asset) { + return { + available: false, + reason: `${assetId} is not available in ${country}` + }; + } + + return { + available: true, + networks: asset.networks + }; + } catch (error) { + return { + available: false, + reason: `Error checking availability: ${error.message}` + }; + } +} + +// Usage +const ethAvailability = await checkAssetAvailability('US', 'ETH', 'CA'); + +if (ethAvailability.available) { + console.log(`ETH available on networks:`, ethAvailability.networks); +} else { + console.log(ethAvailability.reason); +} +``` + +### 6. Dynamic Form Builder + +Build a form that adapts based on available options: + +```tsx +import { fetchOnrampOptions } from '@coinbase/onchainkit/fund'; +import { useState, useEffect } from 'react'; + +function DynamicOnrampForm({ country, subdivision }) { + const [options, setOptions] = useState(null); + const [selectedCurrency, setSelectedCurrency] = useState(''); + const [selectedAsset, setSelectedAsset] = useState(''); + const [selectedNetwork, setSelectedNetwork] = useState(''); + + useEffect(() => { + async function loadOptions() { + const data = await fetchOnrampOptions({ country, subdivision }); + setOptions(data); + + // Set defaults + if (data.fiatCurrencies.length > 0) { + setSelectedCurrency(data.fiatCurrencies[0].id); + } + if (data.assets.length > 0) { + setSelectedAsset(data.assets[0].id); + if (data.assets[0].networks.length > 0) { + setSelectedNetwork(data.assets[0].networks[0].id); + } + } + } + + loadOptions(); + }, [country, subdivision]); + + if (!options) return
Loading...
; + + const selectedAssetData = options.assets.find(a => a.id === selectedAsset); + + return ( +
+
+ + +
+ +
+ + +
+ + {selectedAssetData && ( +
+ + +
+ )} + + +
+ ); +} +``` + +--- + +## Best Practices + +### 1. Performance Optimization + +**Cache Aggressively** +```typescript +// Cache options for at least 30-60 minutes +// Options don't change frequently +const CACHE_TTL = 60 * 60 * 1000; // 1 hour +``` + +**Parallel Requests** +```typescript +// When you need options for multiple regions +const [usOptions, ukOptions] = await Promise.all([ + fetchOnrampOptions({ country: 'US', subdivision: 'CA' }), + fetchOnrampOptions({ country: 'GB' }) +]); +``` + +**Prefetch on Route Load** +```typescript +// In Next.js, prefetch in getServerSideProps or loader +export async function getServerSideProps() { + const options = await fetchOnrampOptions({ + country: 'US', + subdivision: 'CA', + apiKey: process.env.ONRAMP_API_KEY + }); + + return { props: { options } }; +} +``` + +### 2. Error Handling + +**Graceful Fallbacks** +```typescript +async function getOptionsWithFallback( + country: string, + subdivision?: string +) { + try { + return await fetchOnrampOptions({ country, subdivision }); + } catch (error) { + console.error('Failed to fetch options:', error); + + // Return empty options as fallback + return { + fiatCurrencies: [], + assets: [] + }; + } +} +``` + +**Retry Logic** +```typescript +async function fetchWithRetry( + params: FetchOnrampOptionsParams, + maxRetries: number = 3 +): Promise { + let lastError: Error; + + for (let i = 0; i < maxRetries; i++) { + try { + return await fetchOnrampOptions(params); + } catch (error) { + lastError = error as Error; + + if (i < maxRetries - 1) { + // Wait before retrying (exponential backoff) + await new Promise(resolve => + setTimeout(resolve, Math.pow(2, i) * 1000) + ); + } + } + } + + throw lastError; +} +``` + +### 3. Type Safety + +**Strict Typing** +```typescript +import type { OnrampOptionsResponseData } from '@coinbase/onchainkit/fund'; + +interface ProcessedOptions { + currencies: string[]; + assetsByNetwork: Record; +} + +function processOptions( + options: OnrampOptionsResponseData +): ProcessedOptions { + return { + currencies: options.fiatCurrencies.map(c => c.id), + assetsByNetwork: options.assets.reduce((acc, asset) => { + asset.networks.forEach(network => { + if (!acc[network.id]) { + acc[network.id] = []; + } + acc[network.id].push(asset.id); + }); + return acc; + }, {} as Record) + }; +} +``` + +**Type Guards** +```typescript +function isValidCountryCode(code: string): boolean { + return /^[A-Z]{2}$/.test(code); +} + +function isValidSubdivisionCode(code: string): boolean { + return /^[A-Z]{2}$/.test(code); +} + +async function safelyFetchOptions( + country: string, + subdivision?: string +) { + if (!isValidCountryCode(country)) { + throw new Error(`Invalid country code: ${country}`); + } + + if (subdivision && !isValidSubdivisionCode(subdivision)) { + throw new Error(`Invalid subdivision code: ${subdivision}`); + } + + return fetchOnrampOptions({ country, subdivision }); +} +``` + +### 4. User Experience + +**Loading States** +```tsx +function LoadingAwareSelector({ country, subdivision }) { + const [options, setOptions] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + async function load() { + setLoading(true); + const data = await fetchOnrampOptions({ country, subdivision }); + setOptions(data); + setLoading(false); + } + load(); + }, [country, subdivision]); + + if (loading) { + return ( +
+
+
+
+ ); + } + + return ; +} +``` + +**Progressive Enhancement** +```tsx +function ProgressiveOnrampFlow() { + const [step, setStep] = useState(1); + const [country, setCountry] = useState(''); + const [options, setOptions] = useState(null); + + // Step 1: Get country + if (step === 1) { + return ( + { + setCountry(c); + setStep(2); + }} + /> + ); + } + + // Step 2: Load options and show asset selector + if (step === 2) { + return ( + setStep(3)} + /> + ); + } + + // Step 3: Complete purchase + return ; +} +``` + +### 5. Security + +**Server-Side API Keys** +```typescript +// ✅ Good: Server-side only +// pages/api/onramp-options.ts +export default async function handler(req, res) { + const options = await fetchOnrampOptions({ + country: req.query.country, + subdivision: req.query.subdivision, + apiKey: process.env.ONRAMP_API_KEY // Server-side only + }); + + res.json(options); +} + +// ❌ Bad: Client-side API key exposure +function ClientComponent() { + const options = await fetchOnrampOptions({ + country: 'US', + apiKey: 'pk_live_123...' // Never do this! + }); +} +``` + +**Input Validation** +```typescript +import { z } from 'zod'; + +const LocationSchema = z.object({ + country: z.string().length(2).regex(/^[A-Z]{2}$/), + subdivision: z.string().length(2).regex(/^[A-Z]{2}$/).optional() +}); + +async function validatedFetchOptions(params: unknown) { + const validated = LocationSchema.parse(params); + return fetchOnrampOptions({ + ...validated, + apiKey: process.env.ONRAMP_API_KEY + }); +} +``` + +--- + +## Error Handling + +### Common Errors + +#### 1. Missing API Key + +**Error:** +``` +Error: API key is required ``` -### Troubleshooting +**Solution:** +```typescript +// Ensure API key is provided via provider or parameter +const options = await fetchOnrampOptions({ + country: 'US', + subdivision: 'CA', + apiKey: process.env.ONRAMP_API_KEY // Add this +}); +``` -- Ensure Node.js v19+ is installed and that you run `mint dev` from the directory containing `docs.json` (usually `docs/`). -- Local preview differs from production: run `mint update` to update the CLI. +#### 2. Invalid Country Code -## How to contribute +**Error:** +``` +Error: Invalid country code +``` -1. **Fork and branch**: Fork `base/docs` and create a descriptive branch for your change. -2. **Edit content in `docs/`**: Follow the structure and style guidelines below. Preview locally with the Mint CLI. -3. **Open a pull request**: Provide a clear summary and links to related pages. The docs team and community will review. +**Solution:** +```typescript +// Use valid ISO 3166-1 alpha-2 codes +const options = await fetchOnrampOptions({ + country: 'US', // ✅ Correct + // country: 'USA', // ❌ Wrong - must be 2 letters + subdivision: 'CA' +}); +``` -> Tip: Prefer small, focused PRs. Link related guides and references directly in your content. +#### 3. Missing Required Subdivision -## Documentation structure +**Error:** +``` +Error: Subdivision is required for US residents +``` -### Core principle: maintain existing structure +**Solution:** +```typescript +// Always provide subdivision for US +const options = await fetchOnrampOptions({ + country: 'US', + subdivision: 'CA' // Required for US +}); +``` -> Warning: Do not create new top-level sections. Place all new content within existing folders under `docs/`. +#### 4. Network/API Errors -The Base documentation is organized into established sections (for example: `get-started/`, `learn/`, `base-account/`, `base-app/`, `base-chain/`, `cookbook/`, `mini-apps/`, `onchainkit/`). Fit new content into the most relevant existing section. +**Error:** +``` +Error: Failed to fetch onramp options +``` -### Navigation policy +**Solution:** +```typescript +async function robustFetch(country: string, subdivision?: string) { + try { + return await fetchOnrampOptions({ country, subdivision }); + } catch (error) { + if (error.message.includes('network')) { + // Network error - retry + await new Promise(resolve => setTimeout(resolve, 1000)); + return fetchOnrampOptions({ country, subdivision }); + } + + if (error.message.includes('rate limit')) { + // Rate limited - wait longer + await new Promise(resolve => setTimeout(resolve, 5000)); + return fetchOnrampOptions({ country, subdivision }); + } + + throw error; + } +} +``` + +### Comprehensive Error Handler + +```typescript +interface ErrorResult { + success: false; + error: string; + code: string; +} + +interface SuccessResult { + success: true; + data: OnrampOptionsResponseData; +} + +type FetchResult = SuccessResult | ErrorResult; -> Note: We generally do not change the global navigation (top-level tabs) or sidebar sections unless there is a clear, broadly beneficial need. Contributions should focus on improving existing pages and adding new pages within current sections. +async function safeFetchOnrampOptions( + country: string, + subdivision?: string +): Promise { + try { + // Validate inputs + if (!country || country.length !== 2) { + return { + success: false, + error: 'Invalid country code. Must be 2-letter ISO code.', + code: 'INVALID_COUNTRY' + }; + } -### Section purpose and placement + if (country === 'US' && !subdivision) { + return { + success: false, + error: 'Subdivision required for US residents.', + code: 'MISSING_SUBDIVISION' + }; + } -- **Quickstart**: End-to-end setup to first success. Keep concise and current. -- **Concepts**: Explanations of components, architecture, and design philosophy. -- **Guides**: Step-by-step, action-oriented tutorials for specific tasks. -- **Examples**: Complete, runnable examples demonstrating real-world usage. -- **Technical Reference**: API/method/component specs with parameters and return types. -- **Contribute**: Information for contributors and process updates. + // Fetch options + const data = await fetchOnrampOptions({ + country, + subdivision, + apiKey: process.env.ONRAMP_API_KEY + }); -#### Cookbook scope + return { + success: true, + data + }; -- The `cookbook/` section hosts use case-focused guides and patterns, not product-specific documentation. -- Prefer cross-cutting solutions that illustrate how to build on Base across tools and scenarios. + } catch (error) { + console.error('fetchOnrampOptions error:', error); -> Warning: Avoid subsection proliferation: -> - Put all guides at the same level within the Guides section. -> - Organize Reference by component/feature, not per use case. -> - Use cross-links instead of adding new structural layers. + if (error.message.includes('API key')) { + return { + success: false, + error: 'API key is missing or invalid.', + code: 'INVALID_API_KEY' + }; + } + + if (error.message.includes('rate limit')) { + return { + success: false, + error: 'Too many requests. Please try again later.', + code: 'RATE_LIMITED' + }; + } + + return { + success: false, + error: 'Failed to fetch onramp options. Please try again.', + code: 'UNKNOWN_ERROR' + }; + } +} + +// Usage +const result = await safeFetchOnrampOptions('US', 'CA'); + +if (result.success) { + console.log('Options:', result.data); +} else { + console.error(`Error [${result.code}]:`, result.error); +} +``` -## Style and formatting +--- -### Writing style +## Country-Specific Guidelines -1. Be concise and consistent; use active voice and second person. -2. Focus on the happy path; mention alternatives briefly where relevant. -3. Use explicit, descriptive headings and filenames. -4. Maintain consistent terminology; introduce abbreviations on first use. +### United States -### AI-friendly content +**Requirements:** +- Subdivision (state code) is **mandatory** +- Different states may have different asset restrictions -- Use clear, explicit language and link related pages directly. -- Prefer bulleted lists for options/steps when not sequential. -- Name and reference libraries and tools explicitly. -- Use semantic, readable URLs and avoid ambiguous abbreviations. +**Example:** +```typescript +// California +const caOptions = await fetchOnrampOptions({ + country: 'US', + subdivision: 'CA' +}); + +// New York +const nyOptions = await fetchOnrampOptions({ + country: 'US', + subdivision: 'NY' +}); +``` + +**State Considerations:** +- New York has BitLicense requirements +- Some states restrict certain assets +- Regulatory landscape varies by state + +### United Kingdom + +**Requirements:** +- No subdivision required +- FCA regulated + +**Example:** +```typescript +const ukOptions = await fetchOnrampOptions({ + country: 'GB' +}); +``` + +### European Union + +**Requirements:** +- Use country-specific codes (DE, FR, IT, etc.) +- No subdivision typically required +- MiCA regulation applies + +**Examples:** +```typescript +// Germany +const deOptions = await fetchOnrampOptions({ country: 'DE' }); + +// France +const frOptions = await fetchOnrampOptions({ country: 'FR' }); + +// Netherlands +const nlOptions = await fetchOnrampOptions({ country: 'NL' }); +``` + +### Asia-Pacific + +**Requirements vary by country:** + +```typescript +// Japan +const jpOptions = await fetchOnrampOptions({ country: 'JP' }); + +// Singapore +const sgOptions = await fetchOnrampOptions({ country: 'SG' }); + +// Australia +const auOptions = await fetchOnrampOptions({ country: 'AU' }); + +// South Korea +const krOptions = await fetchOnrampOptions({ country: 'KR' }); +``` -> Checklist: -> - Would a Large Language Model understand and follow this content? -> - Can an engineer copy, paste, and run the examples as-is? +--- + +## Troubleshooting + +### Issue: Empty Assets Array + +**Symptom:** `options.assets` returns an empty array + +**Possible Causes:** +1. The country/region doesn't support crypto purchases +2. Regulatory restrictions in that jurisdiction +3. Service temporarily unavailable in that region + +**Solution:** +```typescript +const options = await fetchOnrampOptions({ country, subdivision }); + +if (options.assets.length === 0) { + console.warn(`No assets available in ${country}`); + // Show appropriate message to user + return 'Crypto purchases are not available in your region'; +} +``` + +### Issue: Unexpected Asset Restrictions + +**Symptom:** Expected assets are missing from the response + +**Cause:** Regional regulatory restrictions + +**Solution:** +```typescript +// Check if specific asset is available +function isAssetAvailable( + options: OnrampOptionsResponseData, + assetId: string +): boolean { + return options.assets.some(asset => asset.id === assetId); +} + +const hasETH = isAssetAvailable(options, 'ETH'); +if (!hasETH) { + console.log('ETH not available in this region'); +} +``` + +### Issue: Stale Data + +**Symptom:** Options seem outdated + +**Cause:** Aggressive caching without invalidation + +**Solution:** +```typescript +// Implement cache invalidation +class CacheWithInvalidation { + private cache = new Map(); + + async getOptions(country: string, subdivision?: string) { + const key = `${country}-${subdivision}`; + const cached = this.cache.get(key); + + if (cached && Date.now() - cached.timestamp < 3600000) { + return cached.data; + } + + const data = await fetchOnrampOptions({ country, subdivision }); + this.cache.set(key, { data, timestamp: Date.now() }); + return data; + } + + invalidateAll() { + this.cache.clear(); + } +} +``` + +### Issue: Slow Response Times + +**Symptom:** Function takes several seconds to respond + +**Solutions:** + +1. **Implement caching:** +```typescript +// Cache at application level +const optionsCache = new Map(); + +async function getCachedOptions(country: string, subdivision?: string) { + const key = `${country}-${subdivision}`; + + if (optionsCache.has(key)) { + return optionsCache.get(key); + } + + const options = await fetchOnrampOptions({ country, subdivision }); + optionsCache.set(key, options); + return options; +} +``` + +2. **Use server-side caching:** +```typescript +// In Next.js with SWR +import useSWR from 'swr'; + +function useOnrampOptions(country: string, subdivision?: string) { + const { data, error } = useSWR( + ['onramp-options', country, subdivision], + () => fetchOnrampOptions({ country, subdivision }), + { + revalidateOnFocus: false, + revalidateOnReconnect: false, + dedupingInterval: 3600000 // 1 hour + } + ); + + return { options: data, loading: !error && !data, error }; +} +``` + +### Debug Helper + +```typescript +async function debugFetchOnrampOptions( + country: string, + subdivision?: string +) { + console.group('fetchOnrampOptions Debug'); + console.log('Parameters:', { country, subdivision }); + console.log('API Key present:', !!process.env.ONRAMP_API_KEY); + console.time('Fetch duration'); + + try { + const options = await fetchOnrampOptions({ + country, + subdivision, + apiKey: process.env.ONRAMP_API_KEY + }); + + console.log('Success!'); + console.log('Fiat currencies:', options.fiatCurrencies.length); + console.log('Assets:', options.assets.length); + console.log('Assets list:', options.assets.map(a => a.id)); + console.timeEnd('Fetch duration'); + + return options; + + } catch (error) { + console.error('Error:', error.message); + console.error('Stack:', error.stack); + console.timeEnd('Fetch duration'); + throw error; + + } finally { + console.groupEnd(); + } +} +``` -### Mintlify formatting +--- -- Start main sections with H2 (`##`) and subsections with H3 (`###`). -- Use fenced code blocks with language and optional filename. -- Wrap images in `` and include `alt` text. -- Use callouts for emphasis: ``, ``, ``, ``, ``. -- For procedures, prefer `` / ``. -- For alternatives, use `` / ``. -- For API docs, use ``, ``, and request/response examples. +## Related Resources -### Code examples +### OnchainKit Documentation +- [OnchainKit Overview](https://onchainkit.xyz) +- [Fund Components](https://onchainkit.xyz/fund/introduction) +- [Type Definitions](https://onchainkit.xyz/fund/types) +- [Getting Started Guide](https://onchainkit.xyz/getting-started) -- Provide complete, runnable examples with realistic data. -- Include proper error handling and edge cases. -- Specify language and filename when helpful. -- Show expected output or verification steps. +### API References +- [`fetchOnrampOptions` API](https://onchainkit.xyz/fund/fetch-onramp-options) +- [`OnrampOptionsResponseData` Type](https://onchainkit.xyz/fund/types#onrampoptionsresponsedata) +- [Coinbase Onramp API Documentation](https://docs.cdp.coinbase.com/onramp) -## Third-party guides policy +### Related Utilities +- `FundButton` - Complete onramp UI component +- `fetchOnrampQuote` - Get pricing for transactions +- `fetchOnrampTransaction` - Check transaction status -> Warning: We generally do not accept guides that primarily document a third-party product. Exceptions require a clear Base-focused use case and a tight integration with Base products. Simply deploying on Base or connecting to Base Account/Base App is not sufficient. +### External Resources +- [ISO 3166-1 Country Codes](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) +- [ISO 3166-2 Subdivision Codes](https://en.wikipedia.org/wiki/ISO_3166-2) +- [Coinbase Developer Platform](https://portal.cdp.coinbase.com) -If your goal is to increase discoverability of your product, please request inclusion on the Base Ecosystem page instead. See the instructions for [updating the Base Ecosystem page](https://github.com/base/web?tab=readme-ov-file#updating-the-base-ecosystem-page). +### Community +- [OnchainKit GitHub](https://github.com/coinbase/onchainkit) +- [Discord Community](https://discord.gg/coinbase) +- [Stack Overflow](https://stackoverflow.com/questions/tagged/onchainkit) -## Review checklist (before submitting a PR) +--- -- [ ] Fits within existing structure (no new top-level sections) -- [ ] Minimal, necessary subsections only -- [ ] Consistent terminology; abbreviations introduced on first use -- [ ] Code examples are complete, runnable, and validated -- [ ] Cross-links to related guides/examples/references are included -- [ ] Uses Mintlify components and heading hierarchy correctly -- [ ] Accessible images with descriptive `alt` text and frames -- [ ] AI-friendly: explicit, link-rich, and easy to follow +## Summary -## Submission process +The `fetchOnrampOptions` utility is essential for building compliant, user-friendly onramp experiences. Key takeaways: -1. Create a PR to `https://github.com/base/docs` with your changes. -2. Include a clear description of the change and impacted pages. -3. Request review from the docs team. -4. Address feedback and iterate. -5. Once approved, changes will be merged and published. +✅ **Always provide country code** (ISO 3166-1 alpha-2) +✅ **Provide subdivision for US users** (state code required) +✅ **Implement caching** to improve performance +✅ **Handle errors gracefully** with fallbacks +✅ **Validate inputs** before making API calls +✅ **Secure API keys** (use environment variables) +✅ **Test with multiple regions** to ensure compatibility -## Publishing changes +By following this guide, you'll be able to integrate Coinbase Onramp seamlessly into your application while respecting regional regulations and providing excellent user experience. -The core team will review opened PRs. The SLA is 2 weeks, generally on a first-come, first-served basis outside of urgent changes. +--- -## Storybook for UI components +**Last Updated:** January 2026 -See `storybook/README.md` for details on local Storybook and component docs. +For the latest information and updates, visit the [official OnchainKit documentation](https://onchainkit.xyz).