> ## Documentation Index
> Fetch the complete documentation index at: https://docs.etherfuse.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Test Swaps

> How to test crypto-to-crypto swap flows in the sandbox environment

export const SwapTutorial = () => {
  const {useState, useEffect, useMemo, useCallback} = React;
  const [isSandbox, setIsSandbox] = useState(true);
  const [apiKey, setApiKey] = useState('');
  useEffect(() => {
    const stored = sessionStorage.getItem('etherfuse_api_key');
    if (stored) setApiKey(stored);
  }, []);
  const handleApiKeyChange = value => {
    setApiKey(value);
    if (value) {
      sessionStorage.setItem('etherfuse_api_key', value);
    } else {
      sessionStorage.removeItem('etherfuse_api_key');
    }
  };
  const getOrgIdFromApiKey = () => {
    if (!apiKey) return null;
    const parts = apiKey.split(':');
    if (parts.length >= 3) {
      const orgId = parts[2];
      if ((/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i).test(orgId)) {
        return orgId;
      }
    }
    return null;
  };
  const orgIdFromApiKey = getOrgIdFromApiKey();
  useEffect(() => {
    if (orgIdFromApiKey && (!customerId || customerId === '')) {
      setCustomerId(orgIdFromApiKey);
      setSelectedCustomer(orgIdFromApiKey);
    }
  }, [orgIdFromApiKey]);
  const baseUrl = isSandbox ? 'https://api.sand.etherfuse.com' : 'https://api.etherfuse.com';
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [lookupAssets, setLookupAssets] = useState(null);
  const [quote, setQuote] = useState(null);
  const [swap, setSwap] = useState(null);
  const [customers, setCustomers] = useState(null);
  const [wallets, setWallets] = useState(null);
  const [selectedCustomer, setSelectedCustomer] = useState('');
  const [selectedWallet, setSelectedWallet] = useState(null);
  const [stepErrors, setStepErrors] = useState({});
  const [selectedBlockchain, setSelectedBlockchain] = useState('stellar');
  const [selectedBond, setSelectedBond] = useState('');
  const [sourceAsset, setSourceAsset] = useState('');
  const [customerId, setCustomerId] = useState('');
  const [publicKey, setPublicKey] = useState('');
  const [sourceAmount, setSourceAmount] = useState('10');
  const [quoteId, setQuoteId] = useState('');
  const [swapId, setSwapId] = useState('');
  const [orgDataLoading, setOrgDataLoading] = useState(false);
  const [assetSource, setAssetSource] = useState('lookup');
  const [currencies, setCurrencies] = useState([]);
  const [selectedCurrency, setSelectedCurrency] = useState('');
  const [assetsLoading, setAssetsLoading] = useState(false);
  const [swapAssets, setSwapAssets] = useState([]);
  const usdcAddresses = {
    sandbox: {
      stellar: {
        symbol: 'USDC',
        identifier: 'USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5',
        isStablebond: false
      },
      solana: {
        symbol: 'USDC',
        identifier: '4D4r1KuS9WxbPCzzAJnWtYUJSSzDEZzrF1CbwajWGVjg',
        isStablebond: false
      },
      base: {
        symbol: 'USDC',
        identifier: '0x036CbD53842c5426634e7929541eC2318f3dCF7e',
        isStablebond: false
      },
      polygon: {
        symbol: 'USDC',
        identifier: '0x9999f7Fea5938fD3b1E26A12c3f2fb024e194f97',
        isStablebond: false
      }
    },
    production: {
      stellar: {
        symbol: 'USDC',
        identifier: 'USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN',
        isStablebond: false
      },
      solana: {
        symbol: 'USDC',
        identifier: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
        isStablebond: false
      },
      base: {
        symbol: 'USDC',
        identifier: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
        isStablebond: false
      },
      polygon: {
        symbol: 'USDC',
        identifier: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359',
        isStablebond: false
      }
    }
  };
  const blockchains = useMemo(() => {
    if (!lookupAssets?.stablebonds) return ['stellar', 'solana', 'base', 'polygon'];
    const chains = new Set();
    lookupAssets.stablebonds.forEach(bond => {
      bond.blockchains?.forEach(bc => chains.add(bc.blockchain));
    });
    return Array.from(chains).sort();
  }, [lookupAssets]);
  const fetchSwapAssets = useCallback(async () => {
    if (assetSource !== 'currency' || !selectedCurrency || !publicKey) {
      setSwapAssets([]);
      return;
    }
    setAssetsLoading(true);
    try {
      const params = new URLSearchParams({
        blockchain: selectedBlockchain,
        currency: selectedCurrency.toLowerCase(),
        wallet: publicKey
      });
      const res = await fetch(baseUrl + '/ramp/assets?' + params.toString(), {
        headers: {
          'Authorization': apiKey
        }
      });
      if (res.ok) {
        const data = await res.json();
        setSwapAssets((data.assets || []).map(a => ({
          symbol: a.symbol,
          identifier: a.identifier,
          balance: a.balance,
          isStablebond: a.currency !== null
        })));
      }
    } catch {
      setSwapAssets([]);
    } finally {
      setAssetsLoading(false);
    }
  }, [assetSource, selectedCurrency, publicKey, selectedBlockchain, baseUrl, apiKey]);
  useEffect(() => {
    fetchSwapAssets();
  }, [fetchSwapAssets]);
  const lookupAvailableAssets = useMemo(() => {
    if (!lookupAssets?.stablebonds) return [];
    const env = isSandbox ? 'sandbox' : 'production';
    const usdc = usdcAddresses[env][selectedBlockchain];
    const assetList = [...usdc ? [usdc] : [], ...lookupAssets.stablebonds.map(bond => {
      const chainInfo = bond.blockchains?.find(bc => bc.blockchain === selectedBlockchain);
      if (chainInfo) {
        return {
          symbol: bond.symbol,
          identifier: chainInfo.tokenIdentifier,
          isStablebond: true
        };
      }
      return null;
    }).filter(Boolean)];
    return assetList;
  }, [lookupAssets, selectedBlockchain, isSandbox]);
  const availableAssets = assetSource === 'currency' ? swapAssets : lookupAvailableAssets;
  const targetAssets = useMemo(() => {
    return availableAssets.filter(a => a.isStablebond && a.identifier !== sourceAsset);
  }, [availableAssets, sourceAsset]);
  const generateUUID = () => {
    return ('xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx').replace(/[xy]/g, function (c) {
      const r = Math.random() * 16 | 0;
      const v = c === 'x' ? r : r & 0x3 | 0x8;
      return v.toString(16);
    });
  };
  useEffect(() => {
    if (!quoteId) setQuoteId(generateUUID());
    if (!swapId) setSwapId(generateUUID());
  }, []);
  const defaultQuote = {
    quoteId: generateUUID(),
    customerId: "YOUR_CUSTOMER_ID",
    blockchain: "stellar",
    quoteAssets: {
      type: "swap",
      sourceAsset: "USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN",
      targetAsset: "CETES:GCRYUGD5NVARGXT56XEZI5CIFCQETYHAPQQTHO203IQZTHDHH4LATMYW"
    },
    sourceAmount: "10"
  };
  const defaultSwap = {
    swapId: generateUUID(),
    publicKey: "YOUR_PUBLIC_KEY",
    quoteId: "WILL_BE_FILLED_FROM_QUOTE"
  };
  const [quoteRequest, setQuoteRequest] = useState(JSON.stringify(defaultQuote, null, 2));
  const [swapRequest, setSwapRequest] = useState(JSON.stringify(defaultSwap, null, 2));
  useEffect(() => {
    if (availableAssets.length === 0) return;
    const usdc = availableAssets.find(a => a.symbol === 'USDC');
    if (usdc && !sourceAsset) {
      setSourceAsset(usdc.identifier);
    } else if (!sourceAsset) {
      setSourceAsset(availableAssets[0].identifier);
    }
    const stablebonds = availableAssets.filter(a => a.isStablebond);
    const cetes = stablebonds.find(b => b.symbol.toUpperCase() === 'CETES');
    if (cetes && !selectedBond) {
      setSelectedBond(cetes.symbol);
    } else if (stablebonds.length > 0 && !selectedBond) {
      setSelectedBond(stablebonds[0].symbol);
    }
  }, [availableAssets]);
  useEffect(() => {
    if (!customerId && orgIdFromApiKey) {
      setCustomerId(orgIdFromApiKey);
    }
  }, [orgIdFromApiKey]);
  const getEffectiveSourceAsset = () => sourceAsset;
  const getEffectiveTargetAsset = () => {
    const asset = availableAssets.find(a => a.symbol === selectedBond);
    return asset?.identifier || '';
  };
  useEffect(() => {
    const effectiveSourceAsset = getEffectiveSourceAsset();
    const effectiveTargetAsset = getEffectiveTargetAsset();
    if (effectiveSourceAsset && effectiveTargetAsset && quoteId) {
      const newQuote = {
        quoteId: quoteId,
        customerId: customerId || "YOUR_CUSTOMER_ID",
        blockchain: selectedBlockchain,
        quoteAssets: {
          type: "swap",
          sourceAsset: effectiveSourceAsset,
          targetAsset: effectiveTargetAsset
        },
        sourceAmount: sourceAmount || "10"
      };
      setQuoteRequest(JSON.stringify(newQuote, null, 2));
    }
  }, [selectedBlockchain, isSandbox, sourceAsset, selectedBond, customerId, sourceAmount, lookupAssets, quoteId]);
  useEffect(() => {
    if (swapId) {
      const newSwap = {
        swapId: swapId,
        publicKey: publicKey || "YOUR_PUBLIC_KEY",
        quoteId: quote?.quoteId || "WILL_BE_FILLED_FROM_QUOTE"
      };
      setSwapRequest(JSON.stringify(newSwap, null, 2));
    }
  }, [publicKey, quote, swapId]);
  useEffect(() => {
    if (selectedCustomer) {
      setCustomerId(selectedCustomer);
    }
  }, [selectedCustomer]);
  useEffect(() => {
    if (selectedWallet) {
      setPublicKey(selectedWallet.publicKey);
      if (selectedWallet.blockchain !== selectedBlockchain) {
        setSelectedBlockchain(selectedWallet.blockchain);
      }
    }
  }, [selectedWallet]);
  useEffect(() => {
    if (wallets?.items?.length > 0) {
      const walletsForChain = wallets.items.filter(w => w.blockchain === selectedBlockchain);
      if (walletsForChain.length > 0) {
        if (!selectedWallet || selectedWallet.blockchain !== selectedBlockchain) {
          setSelectedWallet(walletsForChain[0]);
          setPublicKey(walletsForChain[0].publicKey);
        }
      } else {
        setPublicKey('');
      }
    }
  }, [selectedBlockchain, wallets]);
  const makeRequest = async (method, endpoint, body) => {
    setLoading(true);
    setError(null);
    try {
      const options = {
        method,
        headers: {
          'Content-Type': 'application/json',
          'Authorization': apiKey
        }
      };
      if (body) {
        options.body = JSON.stringify(body);
      }
      const response = await fetch(baseUrl + endpoint, options);
      const responseText = await response.text();
      let data = null;
      try {
        if (responseText) {
          data = JSON.parse(responseText);
        }
      } catch (parseErr) {}
      if (!response.ok) {
        let errorMessage = 'HTTP ' + response.status;
        if (data) {
          errorMessage += ': ' + (data.message || data.error || JSON.stringify(data));
        } else if (responseText) {
          errorMessage += ': ' + responseText.substring(0, 200);
        } else {
          errorMessage += ' (no response body)';
        }
        throw new Error(errorMessage);
      }
      return data;
    } catch (err) {
      const msg = err.message || String(err);
      if (msg.includes('Failed to fetch')) {
        setError('Request failed - likely a 401 Unauthorized (CORS headers not sent on error responses). Verify your API key is correct.');
      } else {
        setError(msg);
      }
      throw err;
    } finally {
      setLoading(false);
    }
  };
  const makeSilentRequest = async (method, endpoint, body) => {
    const options = {
      method,
      headers: {
        'Content-Type': 'application/json',
        'Authorization': apiKey
      }
    };
    if (body) {
      options.body = JSON.stringify(body);
    }
    const response = await fetch(baseUrl + endpoint, options);
    if (!response.ok) return null;
    const text = await response.text();
    return text ? JSON.parse(text) : null;
  };
  const fetchOrgData = async () => {
    if (!apiKey) return;
    setOrgDataLoading(true);
    try {
      const customersData = await makeSilentRequest('POST', '/ramp/customers', {
        pageSize: 100,
        pageNumber: 0
      });
      if (customersData?.items) {
        setCustomers(customersData);
        let customerIdToSelect = orgIdFromApiKey || customersData.items[0]?.customerId;
        if (customerIdToSelect) {
          setSelectedCustomer(customerIdToSelect);
          setCustomerId(customerIdToSelect);
          const walletsData = await makeSilentRequest('POST', '/ramp/customer/' + customerIdToSelect + '/wallets', {
            pageSize: 100,
            pageNumber: 0
          });
          if (walletsData?.items) {
            setWallets(walletsData);
            if (walletsData.items.length > 0) {
              const firstWallet = walletsData.items[0];
              setSelectedWallet(firstWallet);
              setPublicKey(firstWallet.publicKey);
              setSelectedBlockchain(firstWallet.blockchain);
            }
          }
        }
      }
    } catch (err) {
      console.error('Failed to fetch org data:', err);
    } finally {
      setOrgDataLoading(false);
    }
  };
  useEffect(() => {
    if (apiKey && apiKey.startsWith('api_')) {
      fetchOrgData();
    }
  }, [apiKey, baseUrl]);
  useEffect(() => {
    const fetchStablebonds = async () => {
      try {
        const response = await fetch(baseUrl + '/lookup/stablebonds');
        if (response.ok) {
          const data = await response.json();
          setLookupAssets(data);
        }
      } catch (err) {
        console.error('Failed to fetch stablebonds:', err);
      }
    };
    fetchStablebonds();
  }, [baseUrl]);
  useEffect(() => {
    const fetchCurrencies = async () => {
      try {
        const response = await fetch(baseUrl + '/lookup/exchange_rate');
        if (response.ok) {
          const data = await response.json();
          const currencyList = Object.keys(data).filter(key => key.startsWith('usd_to_')).map(key => {
            const code = key.replace('usd_to_', '').toUpperCase();
            return {
              currency: code,
              name: code
            };
          }).filter(c => c.currency !== 'USD');
          setCurrencies(currencyList);
        }
      } catch (err) {
        console.error('Failed to fetch currencies:', err);
      }
    };
    fetchCurrencies();
  }, [baseUrl]);
  const fetchWalletsForCustomer = async custId => {
    try {
      const walletsData = await makeRequest('POST', '/ramp/customer/' + custId + '/wallets', {
        pageSize: 100,
        pageNumber: 0
      });
      setWallets(walletsData);
      if (walletsData?.items?.length > 0) {
        const firstWallet = walletsData.items[0];
        setSelectedWallet(firstWallet);
        setPublicKey(firstWallet.publicKey);
        setSelectedBlockchain(firstWallet.blockchain);
      } else {
        setSelectedWallet(null);
        setPublicKey('');
      }
      return walletsData;
    } catch (err) {
      console.error('Failed to fetch wallets for customer:', err);
      setWallets({
        items: []
      });
      return {
        items: []
      };
    }
  };
  const handleCustomerChange = async custId => {
    setSelectedCustomer(custId);
    setCustomerId(custId);
    await fetchWalletsForCustomer(custId);
  };
  const handleWalletChange = walletId => {
    const wallet = wallets?.items?.find(w => w.walletId === walletId);
    if (wallet) {
      setSelectedWallet(wallet);
      setPublicKey(wallet.publicKey);
      setSelectedBlockchain(wallet.blockchain);
    }
  };
  const getWalletsForCustomer = () => {
    if (!wallets?.items) return [];
    return wallets.items;
  };
  const getWalletsForBlockchain = () => {
    if (!wallets?.items) return [];
    return wallets.items.filter(w => w.blockchain === selectedBlockchain);
  };
  const createQuote = async () => {
    setStepErrors(prev => ({
      ...prev,
      step3: null
    }));
    try {
      const body = JSON.parse(quoteRequest);
      const data = await makeRequest('POST', '/ramp/quote', body);
      setQuote(data);
      setQuoteId(generateUUID());
      setSwapId(generateUUID());
      const swapBody = JSON.parse(swapRequest);
      swapBody.quoteId = data.quoteId;
      swapBody.publicKey = publicKey;
      setSwapRequest(JSON.stringify(swapBody, null, 2));
    } catch (err) {
      setStepErrors(prev => ({
        ...prev,
        step3: err.message || String(err)
      }));
    }
  };
  const createSwap = async () => {
    setStepErrors(prev => ({
      ...prev,
      step4: null
    }));
    try {
      const body = JSON.parse(swapRequest);
      const data = await makeRequest('POST', '/ramp/swap', body);
      setSwap(data);
      setSwapId(generateUUID());
    } catch (err) {
      setStepErrors(prev => ({
        ...prev,
        step4: err.message || String(err)
      }));
    }
  };
  const [copied, setCopied] = useState(null);
  const copyToClipboard = (text, id) => {
    navigator.clipboard.writeText(text);
    setCopied(id);
    setTimeout(() => setCopied(null), 2000);
  };
  const getCurl = (method, endpoint, body) => {
    let cmd = 'curl';
    if (method !== 'GET') cmd += ' -X ' + method;
    cmd += ' "' + baseUrl + endpoint + '"';
    cmd += ' \\\n  -H "Authorization: ' + (apiKey || 'YOUR_API_KEY') + '"';
    if (body) {
      cmd += ' \\\n  -H "Content-Type: application/json"';
      cmd += " \\\n  -d '" + JSON.stringify(body) + "'";
    }
    return cmd;
  };
  const getFetch = (method, endpoint, body) => {
    const opts = {
      method,
      headers: {
        'Authorization': apiKey || 'YOUR_API_KEY'
      }
    };
    if (body) {
      opts.headers['Content-Type'] = 'application/json';
      opts.body = 'JSON.stringify(' + JSON.stringify(body) + ')';
    }
    let code = 'const response = await fetch("' + baseUrl + endpoint + '", {\n';
    code += '  method: "' + method + '",\n';
    code += '  headers: {\n';
    code += '    "Authorization": "' + (apiKey || 'YOUR_API_KEY') + '"';
    if (body) {
      code += ',\n    "Content-Type": "application/json"';
    }
    code += '\n  }';
    if (body) {
      code += ',\n  body: JSON.stringify(' + JSON.stringify(body, null, 2).split('\n').join('\n  ') + ')';
    }
    code += '\n});\n';
    code += 'const data = await response.json();';
    return code;
  };
  const buttonStyle = (enabled, completed) => ({
    padding: '10px 20px',
    margin: '5px',
    borderRadius: '6px',
    border: 'none',
    cursor: enabled ? 'pointer' : 'not-allowed',
    backgroundColor: completed ? '#22c55e' : enabled ? '#3b82f6' : '#6b7280',
    color: 'white',
    opacity: loading ? 0.7 : 1,
    fontWeight: 500
  });
  const secondaryButtonStyle = {
    padding: '8px 12px',
    margin: '5px',
    borderRadius: '6px',
    border: '1px solid #d1d5db',
    cursor: 'pointer',
    backgroundColor: 'transparent',
    color: '#6b7280',
    fontSize: '13px'
  };
  const codeBlockStyle = {
    width: '100%',
    maxHeight: '300px',
    overflow: 'auto',
    padding: '16px',
    fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace',
    fontSize: '13px',
    lineHeight: '1.5',
    borderRadius: '8px',
    backgroundColor: '#f8fafc',
    color: '#334155',
    border: '1px solid #e2e8f0',
    margin: 0,
    whiteSpace: 'pre-wrap',
    wordBreak: 'break-word',
    boxSizing: 'border-box'
  };
  const tryItButtonStyle = (enabled, completed) => ({
    display: 'inline-flex',
    alignItems: 'center',
    gap: '6px',
    padding: '8px 16px',
    borderRadius: '20px',
    border: 'none',
    cursor: enabled ? 'pointer' : 'not-allowed',
    backgroundColor: completed ? '#22c55e' : enabled ? '#0d9488' : '#9ca3af',
    color: 'white',
    fontSize: '14px',
    fontWeight: 500,
    opacity: loading ? 0.7 : 1
  });
  const inputStyle = {
    width: '100%',
    padding: '8px 10px',
    borderRadius: '4px',
    border: '1px solid #d1d5db',
    fontSize: '13px',
    fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace',
    boxSizing: 'border-box'
  };
  const selectStyle = {
    ...inputStyle,
    backgroundColor: 'white',
    cursor: 'pointer',
    appearance: 'auto',
    WebkitAppearance: 'menulist',
    MozAppearance: 'menulist'
  };
  const labelStyle = {
    display: 'block',
    fontSize: '12px',
    fontWeight: 600,
    color: '#374151',
    marginBottom: '4px'
  };
  const fieldGroupStyle = {
    marginBottom: '12px'
  };
  const stepErrorStyle = {
    padding: '12px',
    backgroundColor: '#fef2f2',
    color: '#dc2626',
    borderRadius: '6px',
    marginTop: '12px',
    fontSize: '13px',
    border: '1px solid #fecaca'
  };
  const renderStepError = stepKey => {
    const err = stepErrors[stepKey];
    if (!err) return null;
    return <div style={stepErrorStyle}>
        <strong>Error:</strong> {err}
      </div>;
  };
  const renderRandomIdBox = (label, value, onRegenerate) => <div style={fieldGroupStyle}>
      <label style={labelStyle}>{label}</label>
      <div style={{
    display: 'flex',
    alignItems: 'center',
    gap: '8px',
    padding: '8px 10px',
    backgroundColor: '#f0f9ff',
    border: '1px dashed #3b82f6',
    borderRadius: '4px',
    fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace',
    fontSize: '12px',
    color: '#1e40af'
  }}>
        <span style={{
    flex: 1,
    overflow: 'hidden',
    textOverflow: 'ellipsis'
  }}>{value}</span>
        <button onClick={onRegenerate} title="Generate new random ID" style={{
    background: 'none',
    border: 'none',
    cursor: 'pointer',
    fontSize: '16px',
    padding: '2px 6px',
    borderRadius: '4px',
    transition: 'background-color 0.2s'
  }} onMouseOver={e => e.target.style.backgroundColor = '#dbeafe'} onMouseOut={e => e.target.style.backgroundColor = 'transparent'}>
          🎲
        </button>
      </div>
      <div style={{
    fontSize: '10px',
    color: '#6b7280',
    marginTop: '2px'
  }}>
        Auto-generated UUID
      </div>
    </div>;
  return <div style={{
    padding: '20px',
    border: '1px solid #e5e7eb',
    borderRadius: '8px',
    marginTop: '20px'
  }}>
      <div style={{
    display: 'flex',
    justifyContent: 'space-between',
    alignItems: 'center',
    marginBottom: '20px'
  }}>
        <h3 style={{
    margin: 0
  }}>Interactive Swap Tutorial</h3>
        <div style={{
    display: 'flex',
    alignItems: 'center',
    gap: '8px'
  }}>
          <span style={{
    fontSize: '14px',
    color: isSandbox ? '#111827' : '#6b7280',
    fontWeight: isSandbox ? 600 : 400
  }}>Sandbox</span>
          <button onClick={() => setIsSandbox(!isSandbox)} style={{
    position: 'relative',
    width: '48px',
    height: '24px',
    borderRadius: '12px',
    border: 'none',
    backgroundColor: isSandbox ? '#22c55e' : '#3b82f6',
    cursor: 'pointer',
    transition: 'background-color 0.2s'
  }}>
            <span style={{
    position: 'absolute',
    top: '2px',
    left: isSandbox ? '2px' : '26px',
    width: '20px',
    height: '20px',
    borderRadius: '50%',
    backgroundColor: 'white',
    transition: 'left 0.2s'
  }} />
          </button>
          <span style={{
    fontSize: '14px',
    color: isSandbox ? '#6b7280' : '#111827',
    fontWeight: isSandbox ? 400 : 600
  }}>Production</span>
        </div>
      </div>

      <div style={{
    marginBottom: '20px',
    padding: '16px',
    backgroundColor: '#f3f4f6',
    borderRadius: '6px'
  }}>
        <div style={{
    marginBottom: '12px'
  }}>
          <span style={{
    fontSize: '12px',
    color: '#6b7280'
  }}>Host: </span>
          <code style={{
    fontSize: '12px'
  }}>{baseUrl}</code>
        </div>
        <div>
          <div style={{
    display: 'flex',
    alignItems: 'baseline',
    gap: '8px',
    marginBottom: '8px'
  }}>
            <label style={{
    fontWeight: 600
  }}>
              API Key
            </label>
            <span style={{
    fontSize: '12px',
    color: '#6b7280'
  }}>
              Find it under 'API Keys' at{' '}
              <a href={isSandbox ? 'https://devnet.etherfuse.com/ramp?view=manage' : 'https://app.etherfuse.com/ramp?view=manage'} target="_blank" rel="noopener noreferrer" style={{
    color: '#3b82f6'
  }}>
                {isSandbox ? 'devnet.etherfuse.com/ramp' : 'app.etherfuse.com/ramp'}
              </a>
            </span>
          </div>
          <input type="password" value={apiKey} onChange={e => handleApiKeyChange(e.target.value)} placeholder="Enter your API key" style={{
    width: '100%',
    padding: '10px',
    borderRadius: '4px',
    border: '1px solid #d1d5db',
    fontSize: '14px',
    fontFamily: 'monospace',
    boxSizing: 'border-box'
  }} />
        </div>
        {orgIdFromApiKey && <div style={{
    marginTop: '12px',
    padding: '8px 12px',
    backgroundColor: '#f0fdf4',
    borderRadius: '4px',
    border: '1px solid #bbf7d0'
  }}>
            <span style={{
    fontSize: '12px',
    color: '#166534'
  }}>
              <strong>Your org →</strong> {orgIdFromApiKey}
            </span>
            {orgDataLoading && <span style={{
    fontSize: '12px',
    color: '#6b7280',
    marginLeft: '12px'
  }}>Loading org data...</span>}
            {!orgDataLoading && customers?.items?.length > 0 && <span style={{
    fontSize: '12px',
    color: '#166534',
    marginLeft: '12px'
  }}>
                ({customers.items.length} customers, {wallets?.items?.length || 0} wallets)
              </span>}
          </div>}
        <p style={{
    fontSize: '12px',
    color: '#6b7280',
    marginTop: '12px',
    marginBottom: 0
  }}>
          Your API key is stored in your browser's session storage and cleared when you close the tab.
        </p>
      </div>

      {error && <div style={{
    padding: '12px',
    backgroundColor: '#fef2f2',
    color: '#dc2626',
    borderRadius: '6px',
    marginBottom: '16px'
  }}>
          Error: {error}
        </div>}

      {}
      <div style={{
    marginBottom: '24px',
    border: '1px solid #e5e7eb',
    borderRadius: '8px',
    overflow: 'hidden'
  }}>
        {}
        <div style={{
    display: 'flex',
    justifyContent: 'space-between',
    alignItems: 'center',
    padding: '12px 16px',
    backgroundColor: '#f8fafc',
    borderBottom: '1px solid #e5e7eb'
  }}>
          <div style={{
    display: 'flex',
    alignItems: 'center',
    gap: '8px'
  }}>
            <span style={{
    padding: '4px 8px',
    backgroundColor: '#6366f1',
    color: 'white',
    borderRadius: '4px',
    fontSize: '12px',
    fontWeight: 600
  }}>Step 1</span>
            <span style={{
    fontSize: '14px',
    fontWeight: 500
  }}>Select Customer & Wallet</span>
          </div>
          {orgDataLoading && <span style={{
    fontSize: '12px',
    color: '#6b7280'
  }}>Loading...</span>}
        </div>
        {}
        <div style={{
    padding: '16px'
  }}>
          <p style={{
    color: '#6b7280',
    margin: 0,
    marginBottom: '16px'
  }}>
            Select the customer and wallet for the swap. Data is auto-loaded when you enter your API key.
          </p>
          {customers?.items?.length > 0 && <div style={{
    marginTop: '16px'
  }}>
              <div style={{
    display: 'flex',
    alignItems: 'center',
    gap: '8px',
    marginBottom: '8px'
  }}>
                <span style={{
    padding: '4px 8px',
    backgroundColor: '#dcfce7',
    color: '#166534',
    borderRadius: '4px',
    fontSize: '12px',
    fontWeight: 500
  }}>200</span>
                <span style={{
    fontSize: '13px',
    color: '#6b7280'
  }}>Found {customers.items?.length || 0} customers, {wallets?.items?.length || 0} wallets for selected customer</span>
              </div>
              
              {}
              <div style={{
    backgroundColor: '#f9fafb',
    padding: '16px',
    borderRadius: '6px',
    border: '1px solid #e5e7eb'
  }}>
                <div style={{
    display: 'grid',
    gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))',
    gap: '12px'
  }}>
                  {}
                  <div style={fieldGroupStyle}>
                    <label style={labelStyle}>Customer</label>
                    <select value={selectedCustomer} onChange={e => handleCustomerChange(e.target.value)} style={selectStyle}>
                      {orgIdFromApiKey && <option value={orgIdFromApiKey}>
                          Your org → {customers.items?.find(c => c.customerId === orgIdFromApiKey)?.displayName || orgIdFromApiKey.substring(0, 8) + '...'}
                        </option>}
                      {customers.items?.filter(c => c.customerId !== orgIdFromApiKey).map(c => <option key={c.customerId} value={c.customerId}>
                          {c.displayName || c.customerId.substring(0, 8) + '...'}
                        </option>)}
                    </select>
                    <div style={{
    fontSize: '11px',
    color: '#6b7280',
    marginTop: '4px',
    fontFamily: 'monospace'
  }}>
                      {selectedCustomer}
                    </div>
                  </div>

                  {}
                  <div style={fieldGroupStyle}>
                    <label style={labelStyle}>Wallet {selectedWallet?.blockchain && <span style={{
    color: '#6b7280'
  }}>({selectedWallet.blockchain})</span>}</label>
                    <select value={selectedWallet?.walletId || ''} onChange={e => handleWalletChange(e.target.value)} style={selectStyle}>
                      {getWalletsForCustomer().map(w => <option key={w.walletId} value={w.walletId}>
                          [{w.blockchain}] {w.publicKey.substring(0, 8)}...{w.publicKey.slice(-4)}
                        </option>)}
                      {getWalletsForCustomer().length === 0 && <option value="">No wallets for this customer</option>}
                    </select>
                    <div style={{
    fontSize: '11px',
    color: '#6b7280',
    marginTop: '4px',
    fontFamily: 'monospace'
  }}>
                      {publicKey || 'No wallet selected'}
                    </div>
                  </div>
                </div>

                {}
                <div style={{
    display: 'flex',
    flexWrap: 'wrap',
    gap: '8px',
    marginTop: '12px'
  }}>
                  <button onClick={() => copyToClipboard(getCurl('POST', '/ramp/customers', {
    pageSize: 100,
    pageNumber: 0
  }), 'curl-customers')} style={secondaryButtonStyle}>
                    {copied === 'curl-customers' ? '✓ Copied' : 'Copy customers curl'}
                  </button>
                  <button onClick={() => copyToClipboard(getFetch('POST', '/ramp/customers', {
    pageSize: 100,
    pageNumber: 0
  }), 'fetch-customers')} style={secondaryButtonStyle}>
                    {copied === 'fetch-customers' ? '✓ Copied' : 'Copy customers fetch'}
                  </button>
                  <button onClick={() => copyToClipboard(getCurl('POST', '/ramp/customer/' + selectedCustomer + '/wallets', {
    pageSize: 100,
    pageNumber: 0
  }), 'curl-wallets')} style={secondaryButtonStyle}>
                    {copied === 'curl-wallets' ? '✓ Copied' : 'Copy wallets curl'}
                  </button>
                  <button onClick={() => copyToClipboard(getFetch('POST', '/ramp/customer/' + selectedCustomer + '/wallets', {
    pageSize: 100,
    pageNumber: 0
  }), 'fetch-wallets')} style={secondaryButtonStyle}>
                    {copied === 'fetch-wallets' ? '✓ Copied' : 'Copy wallets fetch'}
                  </button>
                </div>
              </div>
            </div>}
        </div>
      </div>

      {}
      <div style={{
    marginBottom: '24px',
    border: '1px solid #e5e7eb',
    borderRadius: '8px',
    overflow: 'hidden',
    opacity: selectedWallet ? 1 : 0.5
  }}>
        {}
        <div style={{
    display: 'flex',
    justifyContent: 'space-between',
    alignItems: 'center',
    padding: '12px 16px',
    backgroundColor: '#f8fafc',
    borderBottom: '1px solid #e5e7eb'
  }}>
          <div style={{
    display: 'flex',
    alignItems: 'center',
    gap: '8px'
  }}>
            <span style={{
    padding: '4px 8px',
    backgroundColor: '#6366f1',
    color: 'white',
    borderRadius: '4px',
    fontSize: '12px',
    fontWeight: 600
  }}>Step 2</span>
            <span style={{
    fontSize: '14px',
    fontWeight: 500
  }}>Select Assets</span>
          </div>
        </div>
        {}
        <div style={{
    padding: '16px'
  }}>
          <p style={{
    color: '#6b7280',
    margin: 0,
    marginBottom: '16px'
  }}>
            Choose which assets to swap. You can use the public lookup endpoint or get personalized opportunities via the ramp/assets endpoint.
          </p>

          {selectedWallet && <div style={{
    backgroundColor: '#f9fafb',
    padding: '16px',
    borderRadius: '6px',
    border: '1px solid #e5e7eb'
  }}>
              {}
              <div style={{
    padding: '12px',
    backgroundColor: '#eff6ff',
    border: '1px solid #bfdbfe',
    borderRadius: '6px',
    marginBottom: '16px'
  }}>
                <div style={{
    fontSize: '12px',
    color: '#1e40af',
    marginBottom: '8px'
  }}>
                  <strong>Asset Data Source:</strong> Assets below are populated from the{' '}
                  {assetSource === 'lookup' ? <a href="/api-reference/lookup/stablebonds" style={{
    color: '#2563eb'
  }}>/lookup/stablebonds</a> : <a href="/api-reference/ramp/assets" style={{
    color: '#2563eb'
  }}>/ramp/assets</a>} endpoint.
                  {assetSource === 'currency' && <span> This provides personalized swap opportunities for your customer.</span>}
                </div>

                {}
                <div style={{
    display: 'flex',
    borderRadius: '6px',
    overflow: 'hidden',
    border: '1px solid #3b82f6'
  }}>
                  <button onClick={() => setAssetSource('lookup')} style={{
    flex: 1,
    padding: '8px 12px',
    border: 'none',
    backgroundColor: assetSource === 'lookup' ? '#3b82f6' : 'white',
    color: assetSource === 'lookup' ? 'white' : '#3b82f6',
    cursor: 'pointer',
    fontSize: '12px',
    fontWeight: 500
  }}>
                    Lookup (All Stablebonds)
                  </button>
                  <button onClick={() => setAssetSource('currency')} style={{
    flex: 1,
    padding: '8px 12px',
    border: 'none',
    borderLeft: '1px solid #3b82f6',
    backgroundColor: assetSource === 'currency' ? '#3b82f6' : 'white',
    color: assetSource === 'currency' ? 'white' : '#3b82f6',
    cursor: 'pointer',
    fontSize: '12px',
    fontWeight: 500
  }}>
                    By Currency (Personalized)
                  </button>
                </div>

                {}
                {assetSource === 'currency' && <div style={{
    marginTop: '12px'
  }}>
                    <label style={{
    ...labelStyle,
    marginBottom: '4px'
  }}>Select Currency</label>
                    <select value={selectedCurrency} onChange={e => setSelectedCurrency(e.target.value)} style={{
    ...selectStyle,
    maxWidth: '200px'
  }}>
                      <option value="">Choose a currency...</option>
                      {currencies.map(curr => <option key={curr.currency} value={curr.currency}>{curr.currency}</option>)}
                    </select>
                    {assetsLoading && <span style={{
    fontSize: '11px',
    color: '#6b7280',
    marginLeft: '8px'
  }}>Loading assets...</span>}
                  </div>}

                {}
                {assetSource === 'lookup' && <div style={{
    display: 'flex',
    gap: '8px',
    marginTop: '12px'
  }}>
                    <button onClick={() => copyToClipboard('curl "' + baseUrl + '/lookup/stablebonds"', 'curl-lookup')} style={secondaryButtonStyle}>
                      {copied === 'curl-lookup' ? '✓ Copied' : 'Copy lookup curl'}
                    </button>
                    <button onClick={() => copyToClipboard('const response = await fetch("' + baseUrl + '/lookup/stablebonds");\nconst data = await response.json();', 'fetch-lookup')} style={secondaryButtonStyle}>
                      {copied === 'fetch-lookup' ? '✓ Copied' : 'Copy lookup fetch'}
                    </button>
                  </div>}
                {assetSource === 'currency' && selectedCurrency && <div style={{
    display: 'flex',
    gap: '8px',
    marginTop: '12px'
  }}>
                    <button onClick={() => copyToClipboard('curl "' + baseUrl + '/ramp/assets?blockchain=' + selectedBlockchain + '&currency=' + selectedCurrency.toLowerCase() + '&wallet=' + publicKey + '" \\\n  -H "Authorization: ' + (apiKey || 'YOUR_API_KEY') + '"', 'curl-assets')} style={secondaryButtonStyle}>
                      {copied === 'curl-assets' ? '✓ Copied' : 'Copy assets curl'}
                    </button>
                    <button onClick={() => copyToClipboard('const response = await fetch("' + baseUrl + '/ramp/assets?blockchain=' + selectedBlockchain + '&currency=' + selectedCurrency.toLowerCase() + '&wallet=' + publicKey + '", {\n  headers: { "Authorization": "' + (apiKey || 'YOUR_API_KEY') + '" }\n});\nconst data = await response.json();', 'fetch-assets')} style={secondaryButtonStyle}>
                      {copied === 'fetch-assets' ? '✓ Copied' : 'Copy assets fetch'}
                    </button>
                  </div>}
              </div>

            </div>}
        </div>
      </div>

      {}
      <div style={{
    marginBottom: '24px',
    border: '1px solid #e5e7eb',
    borderRadius: '8px',
    overflow: 'hidden',
    opacity: selectedWallet ? 1 : 0.5
  }}>
        {}
        <div style={{
    display: 'flex',
    justifyContent: 'space-between',
    alignItems: 'center',
    padding: '12px 16px',
    backgroundColor: '#f8fafc',
    borderBottom: '1px solid #e5e7eb'
  }}>
          <div style={{
    display: 'flex',
    alignItems: 'center',
    gap: '8px'
  }}>
            <span style={{
    padding: '4px 8px',
    backgroundColor: '#6366f1',
    color: 'white',
    borderRadius: '4px',
    fontSize: '12px',
    fontWeight: 600
  }}>Step 3</span>
            <span style={{
    padding: '4px 8px',
    backgroundColor: '#3b82f6',
    color: 'white',
    borderRadius: '4px',
    fontSize: '12px',
    fontWeight: 600
  }}>POST</span>
            <span style={{
    fontFamily: 'monospace',
    fontSize: '14px'
  }}>/ramp/quote</span>
          </div>
          <button onClick={createQuote} disabled={!selectedWallet || !apiKey || loading} style={tryItButtonStyle(selectedWallet && apiKey && !loading)}>
            {loading ? 'Loading...' : 'Try it'} {!loading && <span>▶</span>}
          </button>
        </div>
        {}
        <div style={{
    padding: '16px'
  }}>
          <p style={{
    color: '#6b7280',
    margin: 0,
    marginBottom: '16px'
  }}>
            Generate a quote for the swap.{' '}
            <a href="/api-reference/quotes/create-quote" style={{
    color: '#0d9488'
  }}>View API reference →</a>
          </p>

          {}
          {selectedWallet && <div style={{
    backgroundColor: '#f9fafb',
    padding: '16px',
    borderRadius: '6px',
    marginBottom: '16px',
    border: '1px solid #e5e7eb'
  }}>
              {}
              <div style={{
    display: 'flex',
    flexWrap: 'wrap',
    gap: '16px',
    marginBottom: '16px',
    paddingBottom: '16px',
    borderBottom: '1px solid #e5e7eb'
  }}>
                <div style={{
    display: 'flex',
    alignItems: 'center',
    gap: '6px'
  }}>
                  <span style={{
    fontSize: '11px',
    color: '#6b7280',
    textTransform: 'uppercase',
    fontWeight: 600
  }}>Customer:</span>
                  <span style={{
    padding: '4px 8px',
    backgroundColor: '#f3f4f6',
    border: '1px solid #d1d5db',
    borderRadius: '4px',
    fontSize: '11px',
    fontFamily: 'ui-monospace, monospace',
    color: '#374151'
  }}>
                    {customerId ? customerId.substring(0, 8) + '...' : 'Not selected'}
                  </span>
                </div>
                <div style={{
    display: 'flex',
    alignItems: 'center',
    gap: '6px'
  }}>
                  <span style={{
    fontSize: '11px',
    color: '#6b7280',
    textTransform: 'uppercase',
    fontWeight: 600
  }}>Blockchain:</span>
                  <span style={{
    padding: '4px 8px',
    backgroundColor: '#f3f4f6',
    border: '1px solid #d1d5db',
    borderRadius: '4px',
    fontSize: '11px',
    fontFamily: 'ui-monospace, monospace',
    color: '#374151'
  }}>
                    {selectedBlockchain}
                  </span>
                </div>
                <span style={{
    fontSize: '10px',
    color: '#9ca3af',
    alignSelf: 'center'
  }}>from Step 1</span>
              </div>

              {}
              <div style={{
    display: 'grid',
    gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
    gap: '12px',
    marginBottom: '16px'
  }}>
                {}
                <div style={fieldGroupStyle}>
                  <label style={labelStyle}>Source Asset</label>
                  <select value={sourceAsset} onChange={e => setSourceAsset(e.target.value)} style={selectStyle} disabled={assetSource === 'currency' && (!selectedCurrency || assetsLoading)}>
                    {availableAssets.map(asset => <option key={asset.identifier} value={asset.identifier}>{asset.symbol}</option>)}
                    {availableAssets.length === 0 && <option value="">No assets available</option>}
                  </select>
                </div>

                {}
                <div style={fieldGroupStyle}>
                  <label style={labelStyle}>Target Asset</label>
                  <select value={selectedBond} onChange={e => setSelectedBond(e.target.value)} style={selectStyle} disabled={assetSource === 'currency' && (!selectedCurrency || assetsLoading)}>
                    {targetAssets.map(asset => <option key={asset.identifier} value={asset.symbol}>{asset.symbol}</option>)}
                    {targetAssets.length === 0 && <option value="">No assets available</option>}
                  </select>
                </div>

                {}
                <div style={fieldGroupStyle}>
                  <label style={labelStyle}>Amount</label>
                  <input type="text" value={sourceAmount} onChange={e => setSourceAmount(e.target.value)} placeholder="10" style={inputStyle} />
                </div>
              </div>

              {}
              <div style={{
    marginBottom: '16px'
  }}>
                {renderRandomIdBox('Quote ID', quoteId, () => setQuoteId(generateUUID()))}
              </div>

              {}
              <div>
                <label style={labelStyle}>Request Body</label>
                <pre style={{
    ...codeBlockStyle,
    maxHeight: '150px',
    margin: 0
  }}>{quoteRequest}</pre>
              </div>
            </div>}

          <div style={{
    display: 'flex',
    gap: '8px',
    marginTop: '12px'
  }}>
            <button onClick={() => {
    try {
      copyToClipboard(getCurl('POST', '/ramp/quote', JSON.parse(quoteRequest)), 'curl2');
    } catch (e) {}
  }} style={secondaryButtonStyle}>
              {copied === 'curl2' ? '✓ Copied' : 'Copy curl'}
            </button>
            <button onClick={() => {
    try {
      copyToClipboard(getFetch('POST', '/ramp/quote', JSON.parse(quoteRequest)), 'fetch2');
    } catch (e) {}
  }} style={secondaryButtonStyle}>
              {copied === 'fetch2' ? '✓ Copied' : 'Copy fetch'}
            </button>
          </div>
          {renderStepError('step3')}
          {quote && <div>
              <div style={{
    display: 'flex',
    alignItems: 'center',
    gap: '8px',
    marginBottom: '8px'
  }}>
                <span style={{
    padding: '4px 8px',
    backgroundColor: '#dcfce7',
    color: '#166534',
    borderRadius: '4px',
    fontSize: '12px',
    fontWeight: 500
  }}>200</span>
                <span style={{
    fontSize: '13px',
    color: '#6b7280'
  }}>Success</span>
              </div>
              <pre style={codeBlockStyle}>{JSON.stringify(quote, null, 2)}</pre>
            </div>}
        </div>
      </div>

      {}
      <div style={{
    marginBottom: '24px',
    border: '1px solid #e5e7eb',
    borderRadius: '8px',
    overflow: 'hidden',
    opacity: quote ? 1 : 0.5
  }}>
        {}
        <div style={{
    display: 'flex',
    justifyContent: 'space-between',
    alignItems: 'center',
    padding: '12px 16px',
    backgroundColor: '#f8fafc',
    borderBottom: '1px solid #e5e7eb'
  }}>
          <div style={{
    display: 'flex',
    alignItems: 'center',
    gap: '8px'
  }}>
            <span style={{
    padding: '4px 8px',
    backgroundColor: '#6366f1',
    color: 'white',
    borderRadius: '4px',
    fontSize: '12px',
    fontWeight: 600
  }}>Step 4</span>
            <span style={{
    padding: '4px 8px',
    backgroundColor: '#3b82f6',
    color: 'white',
    borderRadius: '4px',
    fontSize: '12px',
    fontWeight: 600
  }}>POST</span>
            <span style={{
    fontFamily: 'monospace',
    fontSize: '14px'
  }}>/ramp/swap</span>
          </div>
          <button onClick={createSwap} disabled={!quote || !apiKey || loading} style={tryItButtonStyle(quote && apiKey && !loading)}>
            {loading ? 'Loading...' : 'Try it'} {!loading && <span>▶</span>}
          </button>
        </div>
        {}
        <div style={{
    padding: '16px'
  }}>
          <p style={{
    color: '#6b7280',
    margin: 0,
    marginBottom: '16px'
  }}>
            Execute the swap with the quote from the previous step.{' '}
            <a href="/api-reference/swaps/create-swap" style={{
    color: '#0d9488'
  }}>View API reference →</a>
          </p>

          {}
          {quote && <div style={{
    backgroundColor: '#f9fafb',
    padding: '16px',
    borderRadius: '6px',
    marginBottom: '16px',
    border: '1px solid #e5e7eb'
  }}>
              <div style={{
    display: 'grid',
    gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))',
    gap: '12px'
  }}>
                {}
                {renderRandomIdBox('Swap ID', swapId, () => setSwapId(generateUUID()))}
                
                {}
                <div style={fieldGroupStyle}>
                  <label style={labelStyle}>Wallet (Public Key)</label>
                  {wallets?.items?.length > 0 ? <select value={publicKey} onChange={e => {
    setPublicKey(e.target.value);
    const wallet = wallets.items.find(w => w.publicKey === e.target.value);
    if (wallet) setSelectedWallet(wallet);
  }} style={selectStyle}>
                      {getWalletsForBlockchain().map(w => <option key={w.walletId} value={w.publicKey}>
                          {w.displayName || w.publicKey.substring(0, 12) + '...' + w.publicKey.slice(-6)}
                        </option>)}
                      {getWalletsForBlockchain().length === 0 && <option value="">No wallets for {selectedBlockchain}</option>}
                    </select> : <input type="text" value={publicKey} onChange={e => setPublicKey(e.target.value)} placeholder="Enter wallet public key" style={inputStyle} />}
                  <div style={{
    fontSize: '11px',
    color: '#6b7280',
    marginTop: '4px',
    fontFamily: 'monospace'
  }}>
                    {publicKey || 'No wallet selected'}
                  </div>
                </div>
              </div>
              
              {}
              <div style={{
    marginTop: '12px'
  }}>
                <label style={labelStyle}>Request Body</label>
                <pre style={{
    ...codeBlockStyle,
    maxHeight: '100px',
    margin: 0
  }}>{swapRequest}</pre>
              </div>
            </div>}

          <div style={{
    display: 'flex',
    gap: '8px',
    marginTop: '12px'
  }}>
            <button onClick={() => {
    try {
      copyToClipboard(getCurl('POST', '/ramp/swap', JSON.parse(swapRequest)), 'curl3');
    } catch (e) {}
  }} style={secondaryButtonStyle}>
              {copied === 'curl3' ? '✓ Copied' : 'Copy curl'}
            </button>
            <button onClick={() => {
    try {
      copyToClipboard(getFetch('POST', '/ramp/swap', JSON.parse(swapRequest)), 'fetch3');
    } catch (e) {}
  }} style={secondaryButtonStyle}>
              {copied === 'fetch3' ? '✓ Copied' : 'Copy fetch'}
            </button>
          </div>
          {renderStepError('step4')}
          {swap && <div>
              <div style={{
    display: 'flex',
    alignItems: 'center',
    gap: '8px',
    marginBottom: '8px'
  }}>
                <span style={{
    padding: '4px 8px',
    backgroundColor: '#dcfce7',
    color: '#166534',
    borderRadius: '4px',
    fontSize: '12px',
    fontWeight: 500
  }}>200</span>
                <span style={{
    fontSize: '13px',
    color: '#6b7280'
  }}>Success</span>
              </div>
              <pre style={codeBlockStyle}>{JSON.stringify(swap, null, 2)}</pre>
              <div style={{
    padding: '12px',
    backgroundColor: '#f0fdf4',
    color: '#16a34a',
    borderRadius: '6px',
    marginTop: '12px'
  }}>
                ✓ Swap created! Sign the <code>sendTransaction</code> and submit it to the blockchain.
              </div>
            </div>}
        </div>
      </div>

          </div>;
};

<Info>
  **Prerequisite:** You need a KYC-approved wallet on your organization. See [Registering Wallets & Bank Accounts](/guides/registering-accounts). Swaps do not require a bank account.
  For onboarding your customers, see [Onboarding Customers](/guides/onboarding).
</Info>

<Warning>
  Swaps are currently available on **Stellar** and **Solana**. EVM chains (Base, Polygon, Monad) support direct on/off-ramps but do not yet support crypto-to-crypto swaps.
</Warning>

## Swap Flow

<Steps>
  <Step title="Discover Assets">
    <Expandable title="Details">
      Either of these endpoints will give you the asset identifiers needed for quotes:

      * [GET /ramp/assets](/api-reference/assets/get-rampable-assets) — Requires auth. Returns all rampable stablecoins and stablebonds for the specified blockchain. The `currency` parameter controls sort priority (matching assets appear first). Best for building integrations where you already know the customer's wallet.

      * [GET /lookup/stablebonds](/api-reference/lookup/get-all-stablebonds) — Public, no auth. Returns all stablebonds across all chains with pricing and supply data. Best for exploring what's available before onboarding a customer.

      Use the `identifier` from either response as the asset value in your quote.

      **Swap constraint:** The target asset must be a stablebond. The source asset can be any token (typically USDC).

      <Warning>
        **Asset identifier format differs by chain:**

        * **Solana** — Base58 mint address (e.g. `AvvetPGuuB5FD5m86fpw3LtDKyQoUFT1mG9WarNQLW4q`)
        * **Stellar** — `CODE:ISSUER` format (e.g. `CETES:GC3CW7...`)

        Using `SYMBOL:0x...` (Stellar format) on other chains will return `UnsupportedBlockchain`.
      </Warning>

      <Tip>
        **USDC addresses for swap sources:**

        * **Stellar sandbox:** `USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5`
        * **Stellar production:** `USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN`
        * **Solana sandbox:** `4D4r1KuS9WxbPCzzAJnWtYUJSSzDEZzrF1CbwajWGVjg`
        * **Solana production:** `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v`
      </Tip>
    </Expandable>
  </Step>

  <Step title="Create a Swap Quote — POST /ramp/quote">
    <Expandable title="Details">
      Quotes expire after **2 minutes**. For swaps, set `type: "swap"` with the source token identifier as `sourceAsset` and the stablebond identifier as `targetAsset`. The response includes `requiresSwap: true` and the `exchangeRate` between the two assets. See [POST /ramp/quote](/api-reference/quotes/get-quote-for-conversion) for the full schema.

      <CodeGroup>
        ```bash Sandbox theme={null}
        curl -X POST https://api.sand.etherfuse.com/ramp/quote \
          -H "Authorization: <api_key>" \
          -H "Content-Type: application/json" \
          -d '{
            "quoteId": "<uuid>",        # You generate this UUID
            "customerId": "<org_uuid>",  # From onboarding
            "blockchain": "stellar",
            "quoteAssets": {
              "type": "swap",
              "sourceAsset": "USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
              "targetAsset": "CETES:GC3CW7EDYRTWQ635VDIGY6S4ZUF5L6TQ7AA4MWS7LEQDBLUSZXV7UPS4"
            },
            "sourceAmount": "10"
          }'
        ```

        ```bash Production theme={null}
        curl -X POST https://api.etherfuse.com/ramp/quote \
          -H "Authorization: <api_key>" \
          -H "Content-Type: application/json" \
          -d '{
            "quoteId": "<uuid>",        # You generate this UUID
            "customerId": "<org_uuid>",  # From onboarding
            "blockchain": "stellar",
            "quoteAssets": {
              "type": "swap",
              "sourceAsset": "USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN",
              "targetAsset": "CETES:GC3CW7EDYRTWQ635VDIGY6S4ZUF5L6TQ7AA4MWS7LEQDBLUSZXV7UPS4"
            },
            "sourceAmount": "10"
          }'
        ```
      </CodeGroup>

      <Tip>
        **Partner fees:** If your organization has a partner fee configured, it is applied automatically. To override per-quote, add `"partnerFeeBps": 100` (0–500) to the request body. See [Partner Fees](/overview#partner-fees) for details.
      </Tip>
    </Expandable>
  </Step>

  <Step title="Create the Swap — POST /ramp/swap">
    <Expandable title="Details">
      Unlike orders, the swap endpoint requires `blockchain` and `publicKey` in the request body (these are not derived from the quote). The endpoint returns an **empty 200** — all swap progress is delivered via webhooks. See [POST /ramp/swap](/api-reference/swap/swap-assets) for the full schema.

      <CodeGroup>
        ```bash Sandbox theme={null}
        curl -X POST https://api.sand.etherfuse.com/ramp/swap \
          -H "Authorization: <api_key>" \
          -H "Content-Type: application/json" \
          -d '{
            "orderId": "<uuid>",               # You generate this UUID
            "quoteId": "<quote_uuid_from_step_2>", # From Step 2
            "publicKey": "<wallet_public_key>",  # From onboarding
            "blockchain": "stellar"
          }'
        ```

        ```bash Production theme={null}
        curl -X POST https://api.etherfuse.com/ramp/swap \
          -H "Authorization: <api_key>" \
          -H "Content-Type: application/json" \
          -d '{
            "orderId": "<uuid>",               # You generate this UUID
            "quoteId": "<quote_uuid_from_step_2>", # From Step 2
            "publicKey": "<wallet_public_key>",  # From onboarding
            "blockchain": "stellar"
          }'
        ```
      </CodeGroup>

      <Info>
        The `orderId` here is a swap identifier you generate — it tracks this swap through webhooks. The optional `targetWallet` field lets you send the swapped tokens to a different wallet than `publicKey`.
      </Info>
    </Expandable>
  </Step>

  <Step title="Sign the Transaction">
    <Expandable title="Details">
      After creating the swap, you'll receive a `swap_updated` webhook with the `sendTransaction` — a pre-built, encoded transaction ready to sign.

      ```json theme={null}
      {
        "swap_updated": {
          "orderId": "<swap_uuid>",
          "customerId": "<org_uuid>",
          "sendTransaction": "AQAAAA...encoded_transaction",
          "status": "created"
        }
      }
      ```

      Sign the `sendTransaction` using a wallet adapter and submit it to the testnet blockchain. Once confirmed, you'll receive another `swap_updated` webhook with `status: "completed"`.

      <Warning>
        **Do not build your own transaction.** Always use the `sendTransaction` provided in the webhook. This transaction is pre-built by Etherfuse with the correct swap parameters.
      </Warning>
    </Expandable>
  </Step>

  <Step title="Verify Completion">
    <Expandable title="Details">
      Monitor swap progress via `swap_updated` webhooks. Status progression: `swap_created` → `swap_funded` → `swap_completed` (or `swap_failed`). Register a webhook for the `swap_updated` event type via [POST /ramp/webhook](/api-reference/webhooks/create-webhook).

      <Info>
        Unlike on/off-ramps, there is no polling endpoint for swap status — webhooks are the only way to track progress.
      </Info>
    </Expandable>
  </Step>
</Steps>

## Interactive Tutorial

Use the interactive tutorial below to test the swap flow with your sandbox API key. Each step builds on the previous one, and you can edit the request bodies before sending.

<SwapTutorial />

<Expandable title="Chain-specific swap examples">
  **Stellar** — USDC to CETES:

  ```json theme={null}
  {
    "blockchain": "stellar",
    "quoteAssets": {
      "type": "swap",
      "sourceAsset": "USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
      "targetAsset": "CETES:GC3CW7EDYRTWQ635VDIGY6S4ZUF5L6TQ7AA4MWS7LEQDBLUSZXV7UPS4"
    }
  }
  ```

  **Solana** — USDC to CETES:

  ```json theme={null}
  {
    "blockchain": "solana",
    "quoteAssets": {
      "type": "swap",
      "sourceAsset": "4D4r1KuS9WxbPCzzAJnWtYUJSSzDEZzrF1CbwajWGVjg",
      "targetAsset": "AvvetPGuuB5FD5m86fpw3LtDKyQoUFT1mG9WarNQLW4q"
    }
  }
  ```
</Expandable>

<Expandable title="Common errors">
  | Error                                          | Cause                                                                   | Fix                                                                                      |
  | ---------------------------------------------- | ----------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
  | `InvalidSwapTarget`                            | Target asset is not a stablebond                                        | Swap target must be a registered stablebond — use identifiers from `/lookup/stablebonds` |
  | `UnsupportedBlockchain`                        | Swaps not yet available on this chain, or using wrong identifier format | Swaps are currently available on Stellar and Solana. Check your asset identifier format. |
  | `Quote not found or expired`                   | Quote TTL is 2 minutes                                                  | Create a fresh quote                                                                     |
  | `Terms and conditions have not been completed` | Wallet not KYC-approved                                                 | Complete the [onboarding flow](/guides/onboarding)                                       |
  | `Unexpected`                                   | Swap provider (Jupiter/SDEX) returned an error                          | Check that the source asset has testnet liquidity against the target                     |
</Expandable>
