Docs

Integrate Frax Ramp

Two ways in: the React package for React apps, the hosted iframe for everything else. Both talk to the same hosted API.

React package

Install the package and import its stylesheet once (e.g. in your root layout):

pnpm add @iqai/ramp-react

Render the widget with a destination address to let users buy frxUSD with fiat:

import '@iqai/ramp-react/styles.css';
import { CRYPTO_ASSET, FIAT_CURRENCY, FraxRampWidget } from '@iqai/ramp-react';

export function Checkout({ wallet }) {
  return (
    <FraxRampWidget
      destinationAddress={wallet.address}
      amount={100}
      fiatCurrency={FIAT_CURRENCY.USD}
      cryptoAsset={CRYPTO_ASSET.FRXUSD}
    />
  );
}

SELL requires wallet ownership proof. Supply the sender and owner addresses and a callback to sign the session message before an order is created. For a proxy sender, FraxRamp verifies its getOwner() on Fraxtal. An optional onOfframpDeposit callback broadcasts the transfer; otherwise the bound sender can send manually and paste the hash.

<FraxRampWidget
  fiatCurrency={FIAT_CURRENCY.USD}
  cryptoAsset={CRYPTO_ASSET.FRXUSD}
  sellWallet={{ senderAddress: wallet.address, ownerAddress: wallet.address }}
  onSignSellSession={(message) => wallet.signMessage(message)}
  onOfframpDeposit={async ({ amount, receiver, token }) => {
    const txHash = await wallet.sendErc20(token, receiver, amount);
    return { txHash };
  }}
/>

Key props

PropTypeDescription
destinationAddressstringWallet that receives crypto on a buy.
amountnumberPrefilled amount.
fiatCurrencyFiatCurrencyFiat side, e.g. FIAT_CURRENCY.USD.
cryptoAssetCryptoAssetCrypto side, e.g. CRYPTO_ASSET.FRXUSD.
defaultSide'BUY' | 'SELL'Which direction the widget opens on.
appearance'light' | 'dark' | 'system'Theme. Defaults to system.
inheritShadcnbooleanInherit your app's shadcn theme tokens.
sellWalletobjectSELL sender and signing owner addresses.
onOfframpWalletfunctionResolve the connected SELL wallet before admission.
onSignSellSessionfunctionSign the SELL ownership challenge.
onOfframpDepositfunctionBroadcast the transfer and return { txHash } immediately.
onOrderStatusChangefunctionReceives the latest order status.
onOrderCreatedfunctionFires once when an order is created — { side, order }.
onErrorfunctionFires on a recoverable flow error — { side, message }.
getLaunchCodefunctionTrusted partners only. Return a launch code from your backend. See Partner sign-in.
publishableKeystringYour app's public key (frx_pk_…). Names your app on orders.

Hosted iframe

For non-React apps, embed the hosted widget and configure it through query params. Pass parentOrigin so the iframe can post events back to your page:

<iframe
  id="frax-ramp"
  src="https://www.fraxramp.xyz/embed?cryptoAsset=FRXUSD&fiatCurrency=USD&parentOrigin=https%3A%2F%2Fapp.example.com"
  style="width: 420px; height: 640px; border: 0"
></iframe>

Query params

ParamDescription
cryptoAssetCrypto asset, e.g. FRXUSD.
fiatCurrencyFiat currency, e.g. USD.
amountPrefilled amount.
defaultSideBUY or SELL.
destinationAddressDestination wallet for a buy.
senderAddressAddress sending the SELL tokens (EOA or proxy).
ownerAddressEOA signing the SELL session; must own the proxy when one is used.
appearancelight, dark, or system.
parentOriginYour app's origin, required to receive events.
publishableKeyYour app's public key (frx_pk_…). Users sign in to Frax Ramp with an emailed code at checkout.
launch1 to use partner sign-in instead.

Selling through the iframe

Set defaultSide=SELL, senderAddress, and ownerAddress. The parent handles wallet proof and deposit broadcast over postMessage:

  1. The iframe sends sell_session_requested with requestId and message. Validate the message's session challenge, sender, owner, and chain, then sign it and return sell_session_result with the same ID and signature (or error).
  2. At the deposit step, it sends sell_deposit_requested with requestId and request (amount, receiver, token, chainId, senderAddress). Verify these match your wallet and supported corridor, broadcast the transfer, and return sell_deposit_result with txHash (or error).
  3. The widget verifies the receipt. Confirmation failures retain the hash, so retries do not send another payment.
const iframe = document.getElementById('frax-ramp');
const rampOrigin = 'https://www.fraxramp.xyz';

window.addEventListener('message', async (event) => {
  if (event.origin !== rampOrigin || event.source !== iframe.contentWindow) return;
  if (event.data?.source !== 'frax-ramp' || event.data.version !== 1) return;
  const { type, requestId, message, request } = event.data;
  if (type !== 'sell_session_requested' && type !== 'sell_deposit_requested') return;
  const responseType = type === 'sell_session_requested'
    ? 'sell_session_result' : 'sell_deposit_result';
  let result;
  try {
    if (type === 'sell_session_requested') {
      // Validate the exact FraxRamp SELL session format and your sender/owner/chain.
      wallet.validateRampSession(message);
      result = { signature: await wallet.signMessage(message) };
    } else {
      // Validate token, treasury, amount, chain, and bound sender before broadcast.
      wallet.validateRampDeposit(request);
      result = { txHash: await wallet.sendErc20(request.token, request.receiver, request.amount) };
    }
  } catch (err) {
    console.error(err);
    // Shown to the user below the widget's headline.
    result = {
      error:
        responseType === 'sell_session_result'
          ? 'Your wallet did not sign the request. Try again.'
          : 'Your wallet did not send it. Try again.',
    };
  }
  iframe.contentWindow.postMessage(
    { source: 'frax-ramp-host', version: 1, type: responseType, requestId, ...result },
    rampOrigin
  );
});

The manual deposit path still requires the ownership-signature handler. A transfer from another address cannot fund the order. The hosted embed uses its configured API; apiBaseUrl query overrides are ignored. Existing orders remain accessible after the 15-minute admission session expires.

Listening for events

The iframe posts messages to your page. Every event carries source: 'frax-ramp' and version: 1. Always verify the origin and the source field before trusting an event:

window.addEventListener('message', (event) => {
  if (event.origin !== 'https://www.fraxramp.xyz') return;
  if (event.data?.source !== 'frax-ramp') return;

  switch (event.data.type) {
    case 'ready':
      // widget mounted
      break;
    case 'resize':
      // event.data.height — adjust the iframe height
      break;
    case 'order_created':
      // event.data.side, event.data.order (orderId, redirect / deposit details)
      break;
    case 'order_status_changed':
      console.log(event.data.status);
      break;
    case 'error':
      console.warn(event.data.side, event.data.message);
      break;
  }
});
Event typePayloadDescription
ready—Widget has mounted and is ready.
resizeheightNew content height in pixels.
order_createdside, orderAn order was created (before funding). Buy carries providerRedirectUrl; sell carries the deposit details.
order_status_changedstatusLatest order status object.
errorside, messageA recoverable error surfaced in the flow.
sell_session_requestedrequestId, messageValidate and sign the wallet proof, then return sell_session_result.
sell_deposit_requestedrequestId, requestHost should send the tokens and post back a sell_deposit_result.
launch_code_requestedrequestId, sidePartner sign-in only. Return a launch_code_result.

Sign-in

Every order belongs to a signed-in Frax Ramp customer. With the hosted iframe and publishableKey, the checkout button reads Sign in to continue: the user enters their email and a 6-digit code, then checkout continues. A returning user who is still signed in skips this. Without publishableKey or partner sign-in, the widget shows prices but cannot check out.

Identity verification

Signing in is not identity verification. When verification is on, a customer without a valid approval sees a Verify identity step before their first payment: a photo of their ID and a selfie, done by Didit in a new tab. The widget continues by itself once the approval arrives, and an approval stays valid for 12 months. Orders without a valid approval are refused by the server, so the step cannot be bypassed. No identity data is ever sent to your app.

Partner sign-in

Trusted partner apps can open the widget for their signed-in user, so the user does not sign in to Frax Ramp again. Your backend proves who the user is. Your frontend only ever sees a short-lived code.

  1. Ask the Frax Ramp team to register your app as a trusted partner. You get a secret key (frx_sk_…). Keep it on your server only. Never ship it to a browser.
  2. Add a backend route for your signed-in users. It calls POST /v1/launch-codes with the secret key and returns the code. Take the user ID, email and wallets from your own session, never from the request body:
// Your backend. `user` comes from your authenticated session.
const response = await fetch('https://www.fraxramp.xyz/v1/launch-codes', {
  method: 'POST',
  headers: {
    authorization: `Bearer ${process.env.FRAX_RAMP_SECRET_KEY}`,
    'content-type': 'application/json',
  },
  body: JSON.stringify({
    tenant: user.tenantId,
    partnerUserId: user.id, // stable ID, not an email
    email: user.email, // required for Peru (Koywe) deposits
    ...(side === 'BUY'
      ? { buy: { destinationAddress: user.depositWallet } }
      : { sell: { senderAddress: user.wallet, ownerAddress: user.signer } }),
  }),
});
const { launchCode } = await response.json(); // one use, expires in 60 seconds
  1. Give the code to the widget when it asks. In React, pass getLaunchCode={({ side }) => fetchLaunchCodeFromYourBackend(side)}. In the iframe, add launch=1 and answer launch_code_requested:
if (event.data.type === 'launch_code_requested') {
  let result;
  try {
    result = { launchCode: await fetchLaunchCodeFromYourBackend(event.data.side) };
  } catch (err) {
    console.error(err);
    result = { error: 'We could not sign you in. Try again.' }; // shown to the user
  }
  iframe.contentWindow.postMessage(
    { source: 'frax-ramp-host', version: 1, type: 'launch_code_result', requestId: event.data.requestId, ...result },
    rampOrigin
  );
}

An error answer is shown to your user below the widget's headline, so send a short message written for them (translated, no stack traces), for example "Add and verify an email on your account to buy".

The code decides the user, the deposit wallet and the SELL wallets. Values that the browser sends cannot change them. Pass the same destinationAddress to the widget: it shows that wallet as locked, and without it BUY reports an integration error. When your user signs out or switches accounts, remount the widget (for example with a React key). SELL still asks your wallet to sign the ownership challenge.

REST API

Both integrations sit on top of a plain REST API. For the full endpoint reference, see the API reference.