Skip to main content
Interact directly with the Paycrest Gateway smart contract for onchain stablecoin-to-fiat (offramp) order creation, settlement, and refunds. This approach gives you full control over the blockchain transactions and is ideal for dapps, wallets, or any EVM-compatible application.
This guide uses Viem for smart contract interactions, which is the recommended Ethereum library for modern applications. Viem provides better TypeScript support, improved performance, and a more intuitive API compared to ethers.js.

Overview

The Gateway contract is a multi-chain EVM-based smart contract that facilitates the onchain lifecycle of payment orders. It empowers users to create off-ramp orders while enabling liquidity providers to facilitate those orders at competitive exchange rates.

Prerequisites

  • Paycrest Sender Account: Register at app.paycrest.io and complete KYB verification. You’ll need your API Key to embed in every order’s encrypted metadata — this links the onchain order to your sender profile and enables webhooks.
  • Ethereum Provider: MetaMask, WalletConnect, or any Web3 provider
  • Viem: For smart contract interactions
  • USDT/USDC Balance: Sufficient token balance for orders
  • Gas Fees: ETH for transaction fees

Connect Wallet

import { createPublicClient, createWalletClient, http, getAddress } from "viem";
import { base } from "viem/chains";

// Connect to user's wallet
const publicClient = createPublicClient({
  chain: base,
  transport: http()
});

const walletClient = await createWalletClient({
  chain: base,
  transport: window.ethereum
});

const [userAddress] = await walletClient.getAddresses();

Initialize Contracts

import { getContract } from "viem";

// Gateway contract configuration
const GATEWAY_ADDRESS = "0x30F6A8457F8E42371E204a9c103f2Bd42341dD0F"; // Base
const USDT_ADDRESS = "0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2"; // USDT on Base

// Contract instances
const gateway = getContract({
  address: GATEWAY_ADDRESS,
  abi: GATEWAY_ABI,
  publicClient,
  walletClient
});

const usdt = getContract({
  address: USDT_ADDRESS,
  abi: USDT_ABI,
  publicClient,
  walletClient
});

Create an Order

async function createOffRampOrder(amount, recipient, refundAddress) {
  try {
    const rate = await getExchangeRate();
    const accountName = await verifyAccount(recipient);
    const messageHash = await encryptRecipientData({
      ...recipient,
      accountName,
      metadata: { apiKey: process.env.PAYCREST_API_KEY }
    });
    await approveUSDT(amount);
    const { request } = await gateway.simulate.createOrder({
      args: [
        USDT_ADDRESS,
        parseUnits(amount, 6),
        rate,
        zeroAddress,
        0n,
        refundAddress,
        messageHash
      ]
    });
    
    const hash = await walletClient.writeContract(request);
    const receipt = await publicClient.waitForTransactionReceipt({ hash });
    const orderId = extractOrderId(receipt);
    return { orderId, transactionHash: receipt.hash };
  } catch (error) {
    console.error("Error creating order:", error);
    throw error;
  }
}

Exchange Rate and Account Verification

async function getExchangeRate() {
  const response = await fetch("https://api.paycrest.io/v2/rates/base/usdt/1/ngn");
  const body = await response.json();
  const rateStr = body.data?.sell?.rate ?? body.data?.buy?.rate;
  return rateStr != null ? Number(rateStr) : NaN;
}

async function verifyAccount(recipient) {
  const response = await fetch("https://api.paycrest.io/v2/verify-account", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      institution: recipient.institution,
      accountIdentifier: recipient.accountIdentifier
    })
  });
  const data = await response.json();
  return data.data;
}

Data Encryption

The recipient object is encrypted with the aggregator’s public key to produce the messageHash passed to the contract. Always include your API Key in the metadata.apiKey field — this is the same key used as the API-Key header in REST requests, and it links the onchain order to your sender profile, enabling webhooks and order attribution.
Your API Key is available in the dashboard at app.paycrest.io.
async function encryptRecipientData(recipient) {
  const response = await fetch("https://api.paycrest.io/v2/pubkey");
  const { data: publicKey } = await response.json();
  const encrypt = new JSEncrypt();
  encrypt.setPublicKey(publicKey);
  const encrypted = encrypt.encrypt(JSON.stringify(recipient));
  if (!encrypted) {
    throw new Error("Failed to encrypt recipient data");
  }
  return encrypted;
}

Token Approval

async function approveUSDT(amount) {
  const usdtAmount = parseUnits(amount, 6);
  const allowance = await usdt.read.allowance([userAddress, GATEWAY_ADDRESS]);
  if (allowance < usdtAmount) {
    const { request } = await usdt.simulate.approve({
      args: [GATEWAY_ADDRESS, usdtAmount]
    });
    const hash = await walletClient.writeContract(request);
    await publicClient.waitForTransactionReceipt({ hash });
  }
}

Get Order Information

async function getOrderInfo(orderId) {
  const orderInfo = await gateway.read.getOrderInfo([orderId]);
  return {
    sender: orderInfo.sender,
    token: orderInfo.token,
    amount: formatUnits(orderInfo.amount, 6),
    isFulfilled: orderInfo.isFulfilled,
    isRefunded: orderInfo.isRefunded,
    refundAddress: orderInfo.refundAddress
  };
}

Event Listening

// Listen for events using viem's watchContractEvent
const unwatchOrderCreated = publicClient.watchContractEvent({
  address: GATEWAY_ADDRESS,
  abi: GATEWAY_ABI,
  eventName: 'OrderCreated',
  onLogs: (logs) => {
    logs.forEach((log) => {
      console.log("Order Created:", {
        sender: log.args.sender,
        token: log.args.token,
        amount: formatUnits(log.args.amount, 6),
        orderId: log.args.orderId,
        rate: log.args.rate
      });
    });
  }
});

const unwatchSettleOut = publicClient.watchContractEvent({
  address: GATEWAY_ADDRESS,
  abi: GATEWAY_ABI,
  eventName: 'SettleOut',
  onLogs: (logs) => {
    logs.forEach((log) => {
      console.log("Order Settled (SettleOut):", { 
        orderId: log.args.orderId, 
        liquidityProvider: log.args.liquidityProvider, 
        settlePercent: log.args.settlePercent 
      });
    });
  }
});

const unwatchOrderRefunded = publicClient.watchContractEvent({
  address: GATEWAY_ADDRESS,
  abi: GATEWAY_ABI,
  eventName: 'OrderRefunded',
  onLogs: (logs) => {
    logs.forEach((log) => {
      console.log("Order Refunded:", { 
        fee: formatUnits(log.args.fee, 6), 
        orderId: log.args.orderId 
      });
    });
  }
});

Error Handling

function handleContractError(error) {
  if (error.code === 4001) {
    throw new Error("User rejected transaction");
  } else if (error.message.includes("insufficient funds")) {
    throw new Error("Insufficient USDT balance or ETH for gas");
  } else if (error.message.includes("execution reverted")) {
    throw new Error("Transaction reverted - check parameters");
  } else {
    throw new Error(`Contract error: ${error.message}`);
  }
}

async function retryTransaction(txFunction, maxRetries = 3) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await txFunction();
    } catch (error) {
      if (attempt === maxRetries) throw error;
      await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
    }
  }
}

Complete Example

import { parseUnits, formatUnits, zeroAddress } from "viem";

class PaycrestGateway {
  constructor(publicClient, walletClient, gatewayAddress, usdtAddress) {
    this.provider = publicClient;
    this.walletClient = walletClient;
    this.gatewayAddress = gatewayAddress;
    this.usdtAddress = usdtAddress;
    this.gateway = null;
    this.usdt = null;
  }

  async initialize() {
    this.gateway = getContract({
      address: this.gatewayAddress,
      abi: GATEWAY_ABI,
      publicClient: this.provider,
      walletClient: this.walletClient
    });
    this.usdt = getContract({
      address: this.usdtAddress,
      abi: USDT_ABI,
      publicClient: this.provider,
      walletClient: this.walletClient
    });
    this.setupEventListeners();
  }

  setupEventListeners() {
    this.provider.watchContractEvent({
      address: this.gatewayAddress,
      abi: GATEWAY_ABI,
      eventName: 'OrderCreated',
      onLogs: this.handleOrderCreated.bind(this)
    });
    this.provider.watchContractEvent({
      address: this.gatewayAddress,
      abi: GATEWAY_ABI,
      eventName: 'SettleOut',
      onLogs: this.handleSettleOut.bind(this)
    });
    this.provider.watchContractEvent({
      address: this.gatewayAddress,
      abi: GATEWAY_ABI,
      eventName: 'OrderRefunded',
      onLogs: this.handleOrderRefunded.bind(this)
    });
  }

  async createOrder(amount, recipient, refundAddress) {
    try {
      const rate = await this.getExchangeRate();
      const accountName = await this.verifyAccount(recipient);
      const messageHash = await this.encryptRecipientData({ ...recipient, accountName });
      await this.approveUSDT(amount);
      const { request } = await this.gateway.simulate.createOrder({
        args: [
          this.usdtAddress,
          parseUnits(amount, 6),
          rate,
          zeroAddress,
          0n,
          refundAddress,
          messageHash
        ]
      });
      const hash = await this.walletClient.writeContract(request);
      const receipt = await this.provider.waitForTransactionReceipt({ hash });
      return { orderId: this.extractOrderId(receipt), hash: receipt.hash };
    } catch (error) {
      handleContractError(error);
    }
  }
  // ... other methods (getExchangeRate, verifyAccount, etc.)
}

// Usage
const paycrest = new PaycrestGateway(
  publicClient,
  walletClient,
  "0xE8bc3B607CfE68F47000E3d200310D49041148Fc",
  "0xdac17f958d2ee523a2206206994597c13d831ec7"
);

await paycrest.initialize();

const order = await paycrest.createOrder(
  "100", // USDT amount
  {
    institution: "GTBINGLA",
    accountIdentifier: "123456789"
  },
  "0x123..." // Refund address
);

Supported Networks

  • Ethereum: USDT, USDC, cNGN
  • Base: USDT, USDC, cNGN (primary recommended network)
  • Arbitrum One: USDT, USDC
  • Polygon: USDT, USDC, cNGN
  • BNB Smart Chain: USDT, USDC, cNGN
  • Lisk: USDT, USDC
  • Celo: USDT, USDC
  • Scroll: USDT, USDC
See Gateway Contract Addresses for deployment addresses per network.
Paycrest supports very low minimum orders ($0.50) and uses cost-effective EVM L2s. Start with small amounts to test your integration before scaling up.
Failed transactions still consume gas. Validate all parameters before submitting transactions.
Choose this method for full onchain control and direct smart contract interaction.