How to use Lookup APIs

Create an initial screen of your app using Lookup APIs
View as Markdown

This is the first step in building an app using Transak Whitelabel APIs. Before a user logs in, creates an account, or touches a KYC form, your app needs real data — which countries are supported, which crypto assets are available, and which currencies are accepted. The Lookup APIs are four public endpoints that give you exactly that data.

What We’ll Build

We are building a simple wallet app that allows a user to select their country, fiat currency, crypto token, and payment method. The screen will show a live quote with fee breakdown and the amount of crypto the user will receive. By the end of this tutorial, you’ll have the data to render a complete quote screen fully wired to live Transak APIs.

API Flow

UI ElementAPI
Country picker + fiat auto-fillGET /countriescurrencyCode
Fiat currency picker (USD ▾)GET /fiat-currencies
Crypto token picker (BTC ▾)GET /crypto-currencies
~0.00149 BTC you receive amountGET /quotecryptoAmount
Network fee / Transaction fee breakdownGET /quotefeeBreakdown

Look Up APIs

Fetch Supported Countries List

Call Get Countries API on app load and keep the result in memory. You use it in two ways: to power a country-of-residence picker, and to look up a country’s default fiat currency so you can pre-fill the fiat selector for your user.

This API helps you identify which countries are supported for KYC verification, quote fetching, and other transactions.

1

Fetch on app load

Call once at initialization. Cache the result in memory.

$curl https://api-gateway-stg.transak.com/api/v2/lookup/countries \
> -H "x-api-key: YOUR_API_KEY" \
> -H "x-user-ip: USER_IP_ADDRESS"
2

Filter allowed countries

Only show countries where isAllowed: true. Never show restricted countries in your picker.

1const allowed = countries.filter(c => c.isAllowed);
3

Build a country-to-currency map

Index the result so you can instantly look up the default fiat when a user picks a country.

1const countryToCurrency = Object.fromEntries(
2 countries.map(c => [c.code, c.currencyCode])
3);
4// countryToCurrency['US'] → 'USD'
5// countryToCurrency['GB'] → 'GBP'
4

Auto-fill fiat on country change

When the user picks a country, immediately set the fiat selector to the matching currency.

1function onCountrySelect(countryCode: string) {
2 const defaultFiat = countryToCurrency[countryCode];
3 setSelectedFiat(defaultFiat); // auto-fills the fiat picker
4}
CountrySelector.tsx
1import { useState, useEffect, useMemo } from "react";
2
3const API_URL = "https://api-gateway-stg.transak.com/api/v2/lookup/countries";
4
5interface Country {
6 name: string;
7 code: string;
8 currencyCode: string;
9 isAllowed: boolean;
10}
11
12interface CountrySelectorProps {
13 apiKey: string;
14 userIp: string;
15 onSelect?: (countryCode: string, defaultFiat: string) => void;
16}
17
18export function CountrySelector({ apiKey, userIp, onSelect }: CountrySelectorProps) {
19 const [countries, setCountries] = useState<Country[]>([]);
20 const [selected, setSelected] = useState<string>("");
21 const [loading, setLoading] = useState(true);
22 const [error, setError] = useState<string | null>(null);
23
24 useEffect(() => {
25 fetch(API_URL, {
26 headers: {
27 "x-api-key": apiKey,
28 "x-user-ip": userIp,
29 },
30 })
31 .then(r => {
32 if (!r.ok) throw new Error(`HTTP ${r.status}`);
33 return r.json();
34 })
35 .then(data => {
36 setCountries(data.data.countries.filter((c: Country) => c.isAllowed));
37 setLoading(false);
38 })
39 .catch(err => {
40 setError(err.message);
41 setLoading(false);
42 });
43 }, []);
44
45 const countryToCurrency = useMemo(
46 () => Object.fromEntries(countries.map(c => [c.code, c.currencyCode])),
47 [countries]
48 );
49
50 function handleChange(code: string) {
51 setSelected(code);
52 onSelect?.(code, countryToCurrency[code]);
53 }
54
55 if (loading) return <p>Loading countries…</p>;
56 if (error) return <p>Failed to load countries: {error}</p>;
57
58 return (
59 <select value={selected} onChange={e => handleChange(e.target.value)}>
60 <option value="">Select country…</option>
61 {countries.map(c => (
62 <option key={c.code} value={c.code}>
63 {c.name}
64 </option>
65 ))}
66 </select>
67 );
68}

Get All Crypto Currencies List

Get Currencies API powers the crypto picker screen. Every supported token is returned with its full details: symbol, name, image, and which networks it’s available on. One token can exist on multiple networks — each combination is its own selectable item.

The same token (e.g. USDC) appears multiple times — once per network. Treat each symbol + network.name pair as a distinct selectable item. When the user picks

1

Fetch the token list

Call once on page load.

$curl https://api-gateway-stg.transak.com/api/v2/lookup/currencies/crypto-currencies \
> -H "x-api-key: YOUR_API_KEY" \
> -H "x-user-ip: USER_IP_ADDRESS"
2

Filter by transaction direction

Keep only entries where isBuyAllowed: true for buy flows, or isSellAllowed: true for sell flows.

1const tokens = response.filter(t =>
2 mode === "buy" ? t.isBuyAllowed : t.isSellAllowed
3);
3

Group by coinId for the token list

The same token appears once per supported network. Group by coinId so users see each token once, then pick a network in a second step.

1const grouped = tokens.reduce((map, t) => {
2 if (!map[t.coinId]) map[t.coinId] = { ...t, networks: [] };
3 map[t.coinId].networks.push(t.network);
4 return map;
5}, {});
6const coins = Object.values(grouped);
4

Build network filter pills

Deduplicate all network.name values to create filter tabs at the top of the picker.

1const networks = [...new Set(tokens.map(t => t.network.name))].sort();
2// → ['arbitrum', 'ethereum', 'polygon', 'solana', ...]
5

Filter by user's country

Cross-reference regionsNotSupported against the user’s country code — hide tokens not available in their region.

1const available = tokens.filter(t =>
2 !t.regionsNotSupported.includes(userCountryCode)
3);
CryptoSelector.tsx
1import { useState, useEffect, useMemo } from "react";
2
3const API_URL = "https://api-gateway-stg.transak.com/api/v2/lookup/currencies/crypto-currencies";
4
5interface Network {
6 name: string;
7 chainId: number;
8 symbol: string;
9}
10
11interface CryptoCurrency {
12 coinId: string;
13 name: string;
14 symbol: string;
15 image: string;
16 isBuyAllowed: boolean;
17 isSellAllowed: boolean;
18 network: Network;
19 regionsNotSupported: string[];
20}
21
22interface GroupedCoin {
23 coinId: string;
24 name: string;
25 symbol: string;
26 image: string;
27 networks: Network[];
28}
29
30interface CryptoSelectorProps {
31 apiKey: string;
32 userIp: string;
33 mode?: "buy" | "sell";
34 userCountryCode?: string;
35 onSelect?: (symbol: string, network: string) => void;
36}
37
38export function CryptoSelector({ apiKey, userIp, mode = "buy", userCountryCode, onSelect }: CryptoSelectorProps) {
39 const [currencies, setCurrencies] = useState<CryptoCurrency[]>([]);
40 const [loading, setLoading] = useState(true);
41 const [error, setError] = useState<string | null>(null);
42 const [search, setSearch] = useState("");
43 const [networkFilter, setNetworkFilter] = useState("");
44 const [selected, setSelected] = useState<GroupedCoin | null>(null);
45
46 useEffect(() => {
47 fetch(API_URL, {
48 headers: {
49 "x-api-key": apiKey,
50 "x-user-ip": userIp,
51 },
52 })
53 .then(r => {
54 if (!r.ok) throw new Error(`HTTP ${r.status}`);
55 return r.json();
56 })
57 .then(data => {
58 const all: CryptoCurrency[] = data.data.cryptoCurrencies;
59 const filtered = all.filter(c => {
60 const directionOk = mode === "buy" ? c.isBuyAllowed : c.isSellAllowed;
61 const regionOk = !userCountryCode || !c.regionsNotSupported.includes(userCountryCode);
62 return directionOk && regionOk;
63 });
64 setCurrencies(filtered);
65 setLoading(false);
66 })
67 .catch(err => {
68 setError(err.message);
69 setLoading(false);
70 });
71 }, [apiKey, userIp, mode, userCountryCode]);
72
73 // Group entries by coinId — one coin can appear once per network
74 const grouped = useMemo<GroupedCoin[]>(() => {
75 const map: Record<string, GroupedCoin> = {};
76 for (const c of currencies) {
77 if (!map[c.coinId]) {
78 map[c.coinId] = { coinId: c.coinId, name: c.name, symbol: c.symbol, image: c.image, networks: [] };
79 }
80 map[c.coinId].networks.push(c.network);
81 }
82 return Object.values(map);
83 }, [currencies]);
84
85 const allNetworks = useMemo(() => {
86 return [...new Set(currencies.map(c => c.network.name))].sort();
87 }, [currencies]);
88
89 const visible = useMemo(() => {
90 return grouped.filter(coin => {
91 const matchSearch = !search ||
92 coin.name.toLowerCase().includes(search.toLowerCase()) ||
93 coin.symbol.toLowerCase().includes(search.toLowerCase());
94 const matchNetwork = !networkFilter ||
95 coin.networks.some(n => n.name === networkFilter);
96 return matchSearch && matchNetwork;
97 });
98 }, [grouped, search, networkFilter]);
99
100 function handleSelect(coin: GroupedCoin) {
101 setSelected(coin);
102 const net = networkFilter && coin.networks.some(n => n.name === networkFilter)
103 ? networkFilter
104 : coin.networks[0].name;
105 onSelect?.(coin.symbol, net);
106 }
107
108 if (loading) return <p>Loading tokens…</p>;
109 if (error) return <p>Failed to load tokens: {error}</p>;
110
111 return (
112 <div>
113 {/* Network filter pills */}
114 <div>
115 <button onClick={() => setNetworkFilter("")}>All</button>
116 {allNetworks.map(n => (
117 <button
118 key={n}
119 onClick={() => setNetworkFilter(n)}
120 style={{ fontWeight: networkFilter === n ? "bold" : "normal" }}
121 >
122 {n}
123 </button>
124 ))}
125 </div>
126
127 {/* Search */}
128 <input
129 type="text"
130 placeholder="Search tokens…"
131 value={search}
132 onChange={e => setSearch(e.target.value)}
133 />
134
135 {/* Token list */}
136 <ul>
137 {visible.length === 0 && <li>No tokens found.</li>}
138 {visible.map(coin => (
139 <li
140 key={coin.coinId}
141 onClick={() => handleSelect(coin)}
142 style={{ fontWeight: selected?.coinId === coin.coinId ? "bold" : "normal" }}
143 >
144 <img
145 src={coin.image}
146 alt={coin.symbol}
147 width={32}
148 height={32}
149 onError={e => { (e.target as HTMLImageElement).src = `https://placehold.co/32x32?text=${coin.symbol[0]}`; }}
150 />
151 <span>{coin.name}</span>
152 <span>{coin.symbol}</span>
153 <span>
154 {networkFilter && coin.networks.some(n => n.name === networkFilter)
155 ? networkFilter
156 : coin.networks[0].name}
157 </span>
158 </li>
159 ))}
160 </ul>
161 </div>
162 );
163}

Fetch Supported Fiat Currencies

In Get Fiat Currencies API populates your fiat picker. Each currency object includes the currency code and symbol, and — critically — which payment methods are available for it. A user buying with USD has different options than a user buying with EUR.

In this API, each payment method has a minAmount and maxAmount. Use these to validate the user’s input before calling the quote endpoint. It also surfaces which payment methods are supported for each currency.

1

Fetch the fiat currency list

Call once on page load.

$curl https://api-gateway-stg.transak.com/api/v2/lookup/currencies/fiat-currencies \
> -H "x-api-key: YOUR_API_KEY" \
> -H "x-user-ip: USER_IP_ADDRESS"
3

Show payment methods for the selected currency

Each currency has a paymentOptions array. Filter to isActive: true and display with min/max limits.

1const activeMethods = selectedFiat.paymentOptions.filter(p => p.isActive);
2// e.g. [{ id: "credit_debit_card", minAmount: 1, maxAmount: 30000 }]
4

Validate amount before calling quote

Use minAmount / maxAmount to show inline errors before firing the quote endpoint.

1const method = activeMethods.find(p => p.id === selectedMethodId);
2if (amount < method.minAmount || amount > method.maxAmount) {
3 showError(`Enter an amount between $${method.minAmount} and $${method.maxAmount}`);
4}
FiatSelector.tsx
1import { useState, useEffect } from "react";
2
3const API_URL = "https://api-gateway-stg.transak.com/api/v2/lookup/currencies/fiat-currencies";
4
5interface PaymentOption {
6 id: string;
7 name: string;
8 isActive: boolean;
9 minAmount: number;
10 maxAmount: number;
11 processingTime: string;
12}
13
14interface FiatCurrency {
15 symbol: string;
16 name: string;
17 icon: string;
18 isPopular: boolean;
19 paymentOptions: PaymentOption[];
20}
21
22interface FiatSelectorProps {
23 apiKey: string;
24 userIp: string;
25 defaultSymbol?: string;
26 onSelect?: (symbol: string, paymentMethod: string) => void;
27}
28
29export function FiatSelector({ apiKey, userIp, defaultSymbol, onSelect }: FiatSelectorProps) {
30 const [currencies, setCurrencies] = useState<FiatCurrency[]>([]);
31 const [selected, setSelected] = useState<FiatCurrency | null>(null);
32 const [selectedMethod, setSelectedMethod] = useState<string>("");
33 const [loading, setLoading] = useState(true);
34 const [error, setError] = useState<string | null>(null);
35
36 useEffect(() => {
37 fetch(API_URL, {
38 headers: {
39 "x-api-key": apiKey,
40 "x-user-ip": userIp,
41 },
42 })
43 .then(r => {
44 if (!r.ok) throw new Error(`HTTP ${r.status}`);
45 return r.json();
46 })
47 .then(data => {
48 const fiats: FiatCurrency[] = data.data.fiatCurrencies;
49 setCurrencies(fiats);
50 // Auto-select the default fiat (e.g. from country picker)
51 const def = fiats.find(f => f.symbol === (defaultSymbol ?? "USD")) ?? fiats[0];
52 setSelected(def);
53 const firstActive = def?.paymentOptions.find(p => p.isActive);
54 setSelectedMethod(firstActive?.id ?? "");
55 setLoading(false);
56 })
57 .catch(err => {
58 setError(err.message);
59 setLoading(false);
60 });
61 }, [apiKey, userIp, defaultSymbol]);
62
63 function handleCurrencyChange(symbol: string) {
64 const fiat = currencies.find(f => f.symbol === symbol) ?? null;
65 setSelected(fiat);
66 const firstActive = fiat?.paymentOptions.find(p => p.isActive);
67 const method = firstActive?.id ?? "";
68 setSelectedMethod(method);
69 if (fiat && method) onSelect?.(fiat.symbol, method);
70 }
71
72 function handleMethodChange(methodId: string) {
73 setSelectedMethod(methodId);
74 if (selected) onSelect?.(selected.symbol, methodId);
75 }
76
77 if (loading) return <p>Loading currencies…</p>;
78 if (error) return <p>Failed to load currencies: {error}</p>;
79
80 const popular = currencies.filter(f => f.isPopular);
81 const others = currencies.filter(f => !f.isPopular);
82 const activeMethods = selected?.paymentOptions.filter(p => p.isActive) ?? [];
83
84 return (
85 <div>
86 {/* Currency dropdown */}
87 <select value={selected?.symbol ?? ""} onChange={e => handleCurrencyChange(e.target.value)}>
88 <optgroup label="Popular">
89 {popular.map(f => (
90 <option key={f.symbol} value={f.symbol}>{f.symbol}{f.name}</option>
91 ))}
92 </optgroup>
93 <optgroup label="All currencies">
94 {others.map(f => (
95 <option key={f.symbol} value={f.symbol}>{f.symbol}{f.name}</option>
96 ))}
97 </optgroup>
98 </select>
99
100 {/* Payment method selector */}
101 {activeMethods.length > 0 && (
102 <div>
103 {activeMethods.map(opt => (
104 <label key={opt.id}>
105 <input
106 type="radio"
107 name="paymentMethod"
108 value={opt.id}
109 checked={selectedMethod === opt.id}
110 onChange={() => handleMethodChange(opt.id)}
111 />
112 {opt.name} — ${opt.minAmount}–${opt.maxAmount} ({opt.processingTime})
113 </label>
114 ))}
115 </div>
116 )}
117 </div>
118 );
119}

Fetch Final Quote

In Get Quote API is the only Lookup API that requires your partner API key. It returns a Quote that includes the full fee breakdown, and most importantly a quoteId. That quoteId is required for every subsequent API call in this series.

This API call lets you fetch a temporary price quote for a cryptocurrency transaction based on the selected fiat currency, cryptocurrency, payment method, and transaction amount. Since cryptocurrency prices are volatile, the returned quote is refreshed every minute to reflect the latest market price.

1

Build the request params

Collect the user’s inputs — fiat amount, fiat currency, crypto currency, network, and payment method — then call the endpoint.

$curl "https://api-gateway-stg.transak.com/api/v2/lookup/quotes\
>?fiatCurrency=USD\
>&cryptoCurrency=ETH\
>&isBuyOrSell=BUY\
>&fiatAmount=100\
>&network=ethereum\
>&paymentMethod=credit_debit_card" \
> -H "x-api-key: YOUR_API_KEY" \
> -H "x-user-ip: USER_IP_ADDRESS"
2

Display the fee breakdown

The response includes cryptoAmount, totalFee, feeBreakdown, and conversionPrice. Show these in a summary card before the user confirms.

1const { cryptoAmount, totalFee, feeBreakdown, quoteId, conversionPrice } = data;
2// feeBreakdown: [{ name: "Network fee", value: 2.00 }, { name: "Transaction fee", value: 1.50 }]
3

Store quoteId immediately

Save the quoteId to state as soon as you receive it. Every API call from Tutorial 02 onwards requires it.

1setQuoteId(data.quoteId);
4

Auto-refresh before expiry

Set a countdown from expiresAt. Refresh the quote ~30 seconds before it expires, or whenever the user changes any input (debounced 400ms).

1const msUntilExpiry = new Date(data.expiresAt).getTime() - Date.now();
2const refreshAt = msUntilExpiry - 30_000;
3setTimeout(() => refetchQuote(), refreshAt);
QuoteCard.tsx
1import { useState, useEffect, useRef } from "react";
2
3const BASE_URL = "https://api-gateway-stg.transak.com/api/v2/lookup/quotes";
4
5interface FeeBreakdown {
6 id: string;
7 name: string;
8 value: number;
9}
10
11interface Quote {
12 quoteId: string;
13 fiatAmount: number;
14 fiatCurrency: string;
15 cryptoAmount: number;
16 cryptoCurrency: string;
17 totalFee: number;
18 feeBreakdown: FeeBreakdown[];
19 conversionPrice: number;
20 expiresAt: string;
21}
22
23interface QuoteCardProps {
24 apiKey: string;
25 userIp: string;
26 fiatCurrency: string;
27 cryptoCurrency: string;
28 network: string;
29 fiatAmount: number;
30 paymentMethod: string;
31 onQuote?: (quoteId: string) => void;
32}
33
34export function QuoteCard({
35 apiKey,
36 userIp,
37 fiatCurrency,
38 cryptoCurrency,
39 network,
40 fiatAmount,
41 paymentMethod,
42 onQuote,
43}: QuoteCardProps) {
44 const [quote, setQuote] = useState<Quote | null>(null);
45 const [loading, setLoading] = useState(false);
46 const [error, setError] = useState<string | null>(null);
47 const debounceTimer = useRef<ReturnType<typeof setTimeout>>();
48 const refreshTimer = useRef<ReturnType<typeof setTimeout>>();
49
50 async function fetchQuote() {
51 if (!fiatCurrency || !cryptoCurrency || !fiatAmount) return;
52 setLoading(true);
53 setError(null);
54 try {
55 const params = new URLSearchParams({
56 fiatCurrency,
57 cryptoCurrency,
58 isBuyOrSell: "BUY",
59 fiatAmount: String(fiatAmount),
60 ...(network && { network }),
61 ...(paymentMethod && { paymentMethod }),
62 });
63 const res = await fetch(`${BASE_URL}?${params}`, {
64 headers: {
65 "x-api-key": apiKey,
66 "x-user-ip": userIp,
67 },
68 });
69 if (!res.ok) {
70 const err = await res.json();
71 throw new Error(err.message ?? `HTTP ${res.status}`);
72 }
73 const { data }: { data: Quote } = await res.json();
74 setQuote(data);
75 onQuote?.(data.quoteId);
76
77 // Auto-refresh 30s before expiry
78 clearTimeout(refreshTimer.current);
79 const msUntilExpiry = new Date(data.expiresAt).getTime() - Date.now();
80 if (msUntilExpiry > 30_000) {
81 refreshTimer.current = setTimeout(fetchQuote, msUntilExpiry - 30_000);
82 }
83 } catch (err: unknown) {
84 setError(err instanceof Error ? err.message : "Quote fetch failed");
85 setQuote(null);
86 } finally {
87 setLoading(false);
88 }
89 }
90
91 useEffect(() => {
92 clearTimeout(debounceTimer.current);
93 debounceTimer.current = setTimeout(fetchQuote, 400);
94 return () => {
95 clearTimeout(debounceTimer.current);
96 clearTimeout(refreshTimer.current);
97 };
98 }, [fiatCurrency, cryptoCurrency, fiatAmount, network, paymentMethod]);
99
100 if (loading) return <p>Fetching quote…</p>;
101 if (error) return <p style={{ color: "red" }}>{error}</p>;
102 if (!quote) return null;
103
104 return (
105 <div>
106 <p>
107 {quote.fiatAmount} {quote.fiatCurrency} → {quote.cryptoAmount} {quote.cryptoCurrency}
108 </p>
109 <p>Rate: 1 {quote.cryptoCurrency} = {quote.conversionPrice.toLocaleString()} {quote.fiatCurrency}</p>
110 <ul>
111 {quote.feeBreakdown.map(fee => (
112 <li key={fee.id}>
113 <span>{fee.name}</span>
114 <span>{fee.value} {quote.fiatCurrency}</span>
115 </li>
116 ))}
117 <li>
118 <strong>Total fees</strong>
119 <strong>{quote.totalFee} {quote.fiatCurrency}</strong>
120 </li>
121 </ul>
122 <small>Quote expires at {new Date(quote.expiresAt).toLocaleTimeString()}</small>
123 </div>
124 );
125}

Final Output

All four components wired together into a single QuoteScreen page. This is what you’ll have by the end of this tutorial.

QuoteScreen.tsx
1import { useState } from "react";
2import { useAppInit } from "../hooks/useAppInit";
3import { CountrySelector } from "../components/CountrySelector";
4import { FiatSelector } from "../components/FiatSelector";
5import { CryptoSelector } from "../components/CryptoSelector";
6import { QuoteCard } from "../components/QuoteCard";
7
8const API_KEY = "YOUR_PARTNER_API_KEY";
9const USER_IP = "USER_IP_ADDRESS"; // pass the real end-user IP from your server
10
11export function QuoteScreen() {
12 const { loading, error } = useAppInit(API_KEY, USER_IP);
13
14 const [countryCode, setCountryCode] = useState<string>("");
15 const [fiatSymbol, setFiatSymbol] = useState<string>("USD");
16 const [paymentMethod, setPaymentMethod] = useState<string>("credit_debit_card");
17 const [cryptoSymbol, setCryptoSymbol] = useState<string>("ETH");
18 const [network, setNetwork] = useState<string>("ethereum");
19 const [fiatAmount, setFiatAmount] = useState<number>(100);
20 const [quoteId, setQuoteId] = useState<string>("");
21
22 if (loading) return <p>Loading app data…</p>;
23 if (error) return <p>Failed to initialise: {error}</p>;
24
25 return (
26 <div>
27 <h2>Buy Crypto</h2>
28
29 {/* Step 1 — Country (auto-fills fiat below) */}
30 <CountrySelector
31 apiKey={API_KEY}
32 userIp={USER_IP}
33 onSelect={(code, defaultFiat) => {
34 setCountryCode(code);
35 setFiatSymbol(defaultFiat);
36 }}
37 />
38
39 {/* Step 2 — Fiat currency + payment method */}
40 <FiatSelector
41 apiKey={API_KEY}
42 userIp={USER_IP}
43 defaultSymbol={fiatSymbol}
44 onSelect={(symbol, method) => {
45 setFiatSymbol(symbol);
46 setPaymentMethod(method);
47 }}
48 />
49
50 {/* Step 3 — Amount */}
51 <input
52 type="number"
53 value={fiatAmount}
54 min={1}
55 onChange={e => setFiatAmount(Number(e.target.value))}
56 />
57
58 {/* Step 4 — Crypto token + network */}
59 <CryptoSelector
60 apiKey={API_KEY}
61 userIp={USER_IP}
62 mode="buy"
63 userCountryCode={countryCode}
64 onSelect={(symbol, net) => {
65 setCryptoSymbol(symbol);
66 setNetwork(net);
67 }}
68 />
69
70 {/* Step 5 — Live quote (auto-refreshes on every input change) */}
71 <QuoteCard
72 apiKey={API_KEY}
73 userIp={USER_IP}
74 fiatCurrency={fiatSymbol}
75 cryptoCurrency={cryptoSymbol}
76 network={network}
77 fiatAmount={fiatAmount}
78 paymentMethod={paymentMethod}
79 onQuote={id => setQuoteId(id)}
80 />
81
82 {/* Continue is only enabled once we have a valid quoteId */}
83 <button disabled={!quoteId}>
84 Continue →
85 </button>
86 </div>
87 );
88}