IntroductionRecipes

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.


Prerequisites


Step-by-Step Integration

1. Install Dependencies

$npm install @coinbase/onchainkit

2. Create Backend Route for Onramp URL Generation

Create a simple API route that generates the onramp URL using your CDP Project ID:

1// app/onramp/route.ts
2import { getOnrampBuyUrl } from "@coinbase/onchainkit/fund";
3
4export async function GET(request: Request) {
5 const { searchParams } = new URL(request.url);
6 const address = searchParams.get("address");
7
8 if (!address) {
9 return Response.json(
10 { error: "Address parameter is required" },
11 { status: 400 },
12 );
13 }
14
15 const projectId = process.env.CDP_PROJECT_ID;
16
17 if (!projectId) {
18 return Response.json(
19 { error: "CDP_PROJECT_ID environment variable is required" },
20 { status: 500 },
21 );
22 }
23
24 const onrampBuyUrl = getOnrampBuyUrl({
25 projectId,
26 addresses: { [address]: ["base"] }, // or ["ethereum"] for mainnet
27 assets: ["USDC"], // or ["ETH"]
28 presetFiatAmount: 20,
29 fiatCurrency: "USD",
30 });
31
32 return Response.json({ onrampBuyUrl });
33}

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

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

  • Keep CDP_PROJECT_ID in environment variables and never expose it client-side.
    Example:

    $# .env.local
    >CDP_PROJECT_ID=your_project_id
  • 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.

  • Protect the /onramp API route with your app’s auth/session middleware so only the signed-in user can request an Onramp URL for their smart-wallet address. (Rate-limit or require a CSRF token if you don’t have full auth in place.)

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