OverviewRecipes

Onramp Funds to Embedded Smart Wallets with Coinbase

This Recipe demonstrates how to integrate the Coinbase Onramp into an app that uses Alchemy Embedded Smart Wallets. It uses the Coinbase Developer Platform and assumes you’ve configured your smart wallet integration using @account-kit/react.

Goal: Let users seamlessly buy crypto (e.g. ETH) via card and fund their smart wallet directly.

Framework: This recipe shows a front-end integration in Next.js. The same component can be dropped into any React app.

Heads-up: Coinbase Onramp only works on mainnet. Running this demo will purchase real USDC on Base. Use a payment method you’re comfortable with.


Starting July 31, 2025, all Coinbase Onramp and Offramp URLs must be securely initialized with the sessionToken parameter.

Prerequisites

  • Smart wallets integrated in your app (see the React quickstart or Core quickstart).

  • Obtain the necessary API keys all created in the Coinbase Developer Platform

    • CDP Project ID
      • NEXT_PUBLIC_CDP_PROJECT_ID
    • CDP API Credentials (used for Secure Initialization with Session Tokens)
      • CDP_API_KEY
      • CDP_API_SECRET

Step-by-Step Integration

1. Install Dependencies

$npm install @coinbase/onchainkit
>npm install @coinbase/cdp-sdk

2. Create the Onramp Component

This opens a Coinbase-hosted onramp UI in a popup where the user can complete the transaction.

1// components/on-ramp.tsx
2import { useEffect, useState } from "react";
3import {
4 setupOnrampEventListeners,
5 getOnrampBuyUrl,
6} from "@coinbase/onchainkit/fund";
7import { useSmartAccountClient } from "@account-kit/react";
8import type { SuccessEventData, OnrampError } from "@coinbase/onchainkit/fund";
9
10function OnRampPartnerCard() {
11 const { address } = useSmartAccountClient({});
12 const [onrampUrl, setOnrampUrl] = useState<string | null>(null);
13 const [isLoading, setIsLoading] = useState(false);
14 const [error, setError] = useState<string | null>(null);
15 const [isComplete, setIsComplete] = useState(false);
16 const [popupWindow, setPopupWindow] = useState<Window | null>(null);
17
18 useEffect(() => {
19 if (!address) return;
20
21 // Generate session token via API
22 const generateSessionToken = async () => {
23 try {
24 const response = await fetch("/api/session", {
25 method: "POST",
26 headers: { "Content-Type": "application/json" },
27 body: JSON.stringify({
28 addresses: [
29 {
30 address: address,
31 blockchains: ["base"],
32 },
33 ],
34 assets: ["USDC"], // or ["ETH"]
35 }),
36 });
37
38 if (!response.ok) {
39 console.log(response);
40 throw new Error("Failed to generate session token");
41 }
42
43 const { token } = await response.json();
44
45 if (!token) return;
46
47 setIsLoading(true);
48 setError(null);
49
50 try {
51 const url = getOnrampBuyUrl({
52 sessionToken: token,
53 presetFiatAmount: 1,
54 fiatCurrency: "USD",
55 });
56 setOnrampUrl(url);
57 } catch (err) {
58 setError(
59 err instanceof Error
60 ? err.message
61 : "Failed to generate onramp URL",
62 );
63 } finally {
64 setIsLoading(false);
65 }
66 } catch (err) {
67 setError(
68 err instanceof Error
69 ? err.message
70 : "Failed to generate session token",
71 );
72 }
73 };
74
75 generateSessionToken();
76 }, [address]);
77
78 useEffect(() => {
79 if (!onrampUrl) return;
80
81 const unsubscribe = setupOnrampEventListeners({
82 onSuccess: (data?: SuccessEventData) => {
83 console.log("Onramp purchase successful:", data);
84 setIsComplete(true);
85 if (popupWindow && !popupWindow.closed) popupWindow.close();
86 },
87 onExit: (err?: OnrampError) => {
88 if (err) setError("Transaction was cancelled or failed");
89 if (popupWindow && !popupWindow.closed) popupWindow.close();
90 },
91 });
92
93 return unsubscribe;
94 }, [onrampUrl, popupWindow]);
95
96 const openOnrampPopup = () => {
97 if (!onrampUrl) return;
98 const popup = window.open(
99 onrampUrl,
100 "coinbase-onramp",
101 "width=500,height=700,scrollbars=yes,resizable=yes,status=yes,location=yes,toolbar=no,menubar=no",
102 );
103 if (popup) setPopupWindow(popup);
104 };
105
106 if (!address) {
107 return (
108 <div className="text-center">
109 <p className="text-gray-600 mb-4">
110 Please connect your wallet to buy crypto.
111 </p>
112 <button
113 disabled
114 className="px-6 py-3 bg-gray-300 text-gray-500 rounded-lg cursor-not-allowed"
115 >
116 Buy Crypto
117 </button>
118 </div>
119 );
120 }
121
122 if (isComplete) {
123 return (
124 <div className="text-center">
125 <div className="text-green-600 mb-4">
126 <svg
127 className="w-12 h-12 mx-auto mb-2"
128 fill="currentColor"
129 viewBox="0 0 20 20"
130 >
131 <path
132 fillRule="evenodd"
133 d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
134 clipRule="evenodd"
135 />
136 </svg>
137 <p className="text-lg font-semibold">Purchase Complete!</p>
138 <p className="text-sm text-gray-600 mb-4">
139 Your transaction has been processed successfully.
140 </p>
141 </div>
142 <button
143 onClick={() => setIsComplete(false)}
144 className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
145 >
146 Buy More Crypto
147 </button>
148 </div>
149 );
150 }
151
152 if (error) {
153 return (
154 <div className="text-center">
155 <p className="text-red-600 mb-4">Error: {error}</p>
156 <button
157 onClick={() => window.location.reload()}
158 className="px-6 py-3 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors"
159 >
160 Retry
161 </button>
162 </div>
163 );
164 }
165
166 return (
167 <div className="text-center">
168 <p className="text-gray-600 mb-4">
169 Purchase cryptocurrency directly to your wallet using Coinbase Onramp.
170 </p>
171 <button
172 onClick={openOnrampPopup}
173 disabled={isLoading || !onrampUrl}
174 className={`px-6 py-3 rounded-lg font-semibold transition-colors ${
175 isLoading || !onrampUrl
176 ? "bg-gray-300 text-gray-500 cursor-not-allowed"
177 : "bg-blue-600 text-white hover:bg-blue-700"
178 }`}
179 >
180 {isLoading ? (
181 <div className="flex items-center gap-2">
182 <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
183 Loading...
184 </div>
185 ) : (
186 "Buy Crypto"
187 )}
188 </button>
189 </div>
190 );
191}
192
193export default OnRampPartnerCard;

3. Generate Session Token

1import { generateJwt } from "@coinbase/cdp-sdk/auth";
2
3const CDP_API_KEY = process.env.CDP_API_KEY;
4const CDP_API_SECRET = process.env.CDP_API_SECRET;
5
6export async function POST(request: Request) {
7 try {
8 const body = await request.json();
9
10 const requestMethod = "POST";
11 const requestHost = "api.developer.coinbase.com";
12 const requestPath = "/onramp/v1/token";
13 let jwt = "";
14
15 try {
16 // Use the CDP SDK to generate the JWT
17 const token = await generateJwt({
18 apiKeyId: CDP_API_KEY!,
19 apiKeySecret: CDP_API_SECRET!,
20 requestMethod: requestMethod,
21 requestHost: requestHost,
22 requestPath: requestPath,
23 expiresIn: 120, // optional (defaults to 120 seconds)
24 });
25
26 jwt = token;
27 } catch (error) {
28 console.error("Error generating JWT:", error);
29 throw error;
30 }
31
32 // Generate session token using CDP API with JWT
33 const response = await fetch(
34 "https://api.developer.coinbase.com/onramp/v1/token",
35 {
36 method: "POST",
37 headers: {
38 Authorization: `Bearer ${jwt}`,
39 "Content-Type": "application/json",
40 },
41 body: JSON.stringify(body),
42 },
43 );
44
45 if (!response.ok) {
46 const errorData = await response.text();
47 return new Response(
48 JSON.stringify({
49 error: `Failed to generate session token: ${response.status} ${errorData}`,
50 }),
51 {
52 status: response.status,
53 headers: { "Content-Type": "application/json" },
54 },
55 );
56 }
57
58 const data = await response.json();
59 return new Response(JSON.stringify({ token: data.token }), {
60 status: 200,
61 headers: { "Content-Type": "application/json" },
62 });
63 } catch (error) {
64 console.error("Session token generation error:", error);
65 return new Response(JSON.stringify({ error: "Internal server error" }), {
66 status: 500,
67 headers: { "Content-Type": "application/json" },
68 });
69 }
70}

4. Add to Your App

1// app/page.tsx
2import OnRampPartnerCard from "./components/on-ramp";
3
4export default function Home() {
5 return (
6 <div className="container mx-auto p-4">
7 {/* Your other components */}
8
9 <div className="bg-white rounded-lg shadow-lg p-6">
10 <h2 className="text-xl font-semibold mb-4">Buy Crypto</h2>
11 <OnRampPartnerCard />
12 </div>
13 </div>
14 );
15}

Best Practices

  • Let users change networks/assets in the Coinbase modal (default here is card ➜ ETH).
  • For user-level analytics you can pass partnerUserId when generating the onramp URL.

Success!

Users can now click “Buy Crypto”, complete their purchase in a popup, and immediately spend the ETH that lands in their smart wallet.

Next Steps

  • Sponsor their first transaction – set up Gas Manager so new users don’t pay gas.

Resources