Skip to content
Alchemy Logo

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.

  • 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

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

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

// components/on-ramp.tsx
import { useEffect, useState } from "react";
import {
  setupOnrampEventListeners,
  getOnrampBuyUrl,
} from "@coinbase/onchainkit/fund";
import { useSmartAccountClient } from "@account-kit/react";
import type { SuccessEventData, OnrampError } from "@coinbase/onchainkit/fund";
 
function OnRampPartnerCard() {
  const { address } = useSmartAccountClient({});
  const [onrampUrl, setOnrampUrl] = useState<string | null>(null);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [isComplete, setIsComplete] = useState(false);
  const [popupWindow, setPopupWindow] = useState<Window | null>(null);
 
  useEffect(() => {
    if (!address) return;
 
    // Generate session token via API
    const generateSessionToken = async () => {
      try {
        const response = await fetch("/api/session", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            addresses: [
              {
                address: address,
                blockchains: ["base"],
              },
            ],
            assets: ["USDC"], // or ["ETH"]
          }),
        });
 
        if (!response.ok) {
          console.log(response);
          throw new Error("Failed to generate session token");
        }
 
        const { token } = await response.json();
 
        if (!token) return;
 
        setIsLoading(true);
        setError(null);
 
        try {
          const url = getOnrampBuyUrl({
            sessionToken: token,
            presetFiatAmount: 1,
            fiatCurrency: "USD",
          });
          setOnrampUrl(url);
        } catch (err) {
          setError(
            err instanceof Error
              ? err.message
              : "Failed to generate onramp URL",
          );
        } finally {
          setIsLoading(false);
        }
      } catch (err) {
        setError(
          err instanceof Error
            ? err.message
            : "Failed to generate session token",
        );
      }
    };
 
    generateSessionToken();
  }, [address]);
 
  useEffect(() => {
    if (!onrampUrl) return;
 
    const unsubscribe = setupOnrampEventListeners({
      onSuccess: (data?: SuccessEventData) => {
        console.log("Onramp purchase successful:", data);
        setIsComplete(true);
        if (popupWindow && !popupWindow.closed) popupWindow.close();
      },
      onExit: (err?: OnrampError) => {
        if (err) setError("Transaction was cancelled or failed");
        if (popupWindow && !popupWindow.closed) popupWindow.close();
      },
    });
 
    return unsubscribe;
  }, [onrampUrl, popupWindow]);
 
  const openOnrampPopup = () => {
    if (!onrampUrl) return;
    const popup = window.open(
      onrampUrl,
      "coinbase-onramp",
      "width=500,height=700,scrollbars=yes,resizable=yes,status=yes,location=yes,toolbar=no,menubar=no",
    );
    if (popup) setPopupWindow(popup);
  };
 
  if (!address) {
    return (
      <div className="text-center">
        <p className="text-gray-600 mb-4">
          Please connect your wallet to buy crypto.
        </p>
        <button
          disabled
          className="px-6 py-3 bg-gray-300 text-gray-500 rounded-lg cursor-not-allowed"
        >
          Buy Crypto
        </button>
      </div>
    );
  }
 
  if (isComplete) {
    return (
      <div className="text-center">
        <div className="text-green-600 mb-4">
          <svg
            className="w-12 h-12 mx-auto mb-2"
            fill="currentColor"
            viewBox="0 0 20 20"
          >
            <path
              fillRule="evenodd"
              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"
              clipRule="evenodd"
            />
          </svg>
          <p className="text-lg font-semibold">Purchase Complete!</p>
          <p className="text-sm text-gray-600 mb-4">
            Your transaction has been processed successfully.
          </p>
        </div>
        <button
          onClick={() => setIsComplete(false)}
          className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
        >
          Buy More Crypto
        </button>
      </div>
    );
  }
 
  if (error) {
    return (
      <div className="text-center">
        <p className="text-red-600 mb-4">Error: {error}</p>
        <button
          onClick={() => window.location.reload()}
          className="px-6 py-3 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors"
        >
          Retry
        </button>
      </div>
    );
  }
 
  return (
    <div className="text-center">
      <p className="text-gray-600 mb-4">
        Purchase cryptocurrency directly to your wallet using Coinbase Onramp.
      </p>
      <button
        onClick={openOnrampPopup}
        disabled={isLoading || !onrampUrl}
        className={`px-6 py-3 rounded-lg font-semibold transition-colors ${
          isLoading || !onrampUrl
            ? "bg-gray-300 text-gray-500 cursor-not-allowed"
            : "bg-blue-600 text-white hover:bg-blue-700"
        }`}
      >
        {isLoading ? (
          <div className="flex items-center gap-2">
            <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
            Loading...
          </div>
        ) : (
          "Buy Crypto"
        )}
      </button>
    </div>
  );
}
 
export default OnRampPartnerCard;

import { generateJwt } from "@coinbase/cdp-sdk/auth";
 
const CDP_API_KEY = process.env.CDP_API_KEY;
const CDP_API_SECRET = process.env.CDP_API_SECRET;
 
export async function POST(request: Request) {
  try {
    const body = await request.json();
 
    const requestMethod = "POST";
    const requestHost = "api.developer.coinbase.com";
    const requestPath = "/onramp/v1/token";
    let jwt = "";
 
    try {
      // Use the CDP SDK to generate the JWT
      const token = await generateJwt({
        apiKeyId: CDP_API_KEY!,
        apiKeySecret: CDP_API_SECRET!,
        requestMethod: requestMethod,
        requestHost: requestHost,
        requestPath: requestPath,
        expiresIn: 120, // optional (defaults to 120 seconds)
      });
 
      jwt = token;
    } catch (error) {
      console.error("Error generating JWT:", error);
      throw error;
    }
 
    // Generate session token using CDP API with JWT
    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(body),
      },
    );
 
    if (!response.ok) {
      const errorData = await response.text();
      return new Response(
        JSON.stringify({
          error: `Failed to generate session token: ${response.status} ${errorData}`,
        }),
        {
          status: response.status,
          headers: { "Content-Type": "application/json" },
        },
      );
    }
 
    const data = await response.json();
    return new Response(JSON.stringify({ token: data.token }), {
      status: 200,
      headers: { "Content-Type": "application/json" },
    });
  } catch (error) {
    console.error("Session token generation error:", error);
    return new Response(JSON.stringify({ error: "Internal server error" }), {
      status: 500,
      headers: { "Content-Type": "application/json" },
    });
  }
}

// app/page.tsx
import OnRampPartnerCard from "./components/on-ramp";
 
export default function Home() {
  return (
    <div className="container mx-auto p-4">
      {/* Your other components */}
 
      <div className="bg-white rounded-lg shadow-lg p-6">
        <h2 className="text-xl font-semibold mb-4">Buy Crypto</h2>
        <OnRampPartnerCard />
      </div>
    </div>
  );
}

  • 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.

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

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

Was this page helpful?