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 on-chain 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

  • 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 = "0xE8bc3B607CfE68F47000E3d200310D49041148Fc";
const USDT_ADDRESS = "0xdac17f958d2ee523a2206206994597c13d831ec7";

// 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 });
    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/v1/rates/usdt/1/ngn");
  const data = await response.json();
  return Number(data.data);
}

async function verifyAccount(recipient) {
  const response = await fetch("https://api.paycrest.io/v1/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

async function encryptRecipientData(recipient) {
  const response = await fetch("https://api.paycrest.io/v1/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 unwatchOrderSettled = publicClient.watchContractEvent({
  address: GATEWAY_ADDRESS,
  abi: GATEWAY_ABI,
  eventName: 'OrderSettled',
  onLogs: (logs) => {
    logs.forEach((log) => {
      console.log("Order Settled:", { 
        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: 'OrderSettled',
      onLogs: this.handleOrderSettled.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

  • Base: Primary network for USDT/USDC transactions
  • Polygon: Cost-effective transactions
  • BNB Smart Chain: Binance ecosystem support
  • Arbitrum One: High-performance L2 network
  • Lisk: Alternative blockchain network
  • Celo: Mobile-first blockchain (CUSD, CNGN)
  • Tron: USDT transactions

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.

Choose this method for full onchain control and direct smart contract interaction.