DEV Community

Cover image for How to build a Crypto Payment Gateway with Fystack Programmable Wallet Infrastructure (Part 1)
Thi Nguyen (T)
Thi Nguyen (T)

Posted on • Originally published at fystack.io

How to build a Crypto Payment Gateway with Fystack Programmable Wallet Infrastructure (Part 1)

Overview

Fystack enables developers to create production-ready cryptocurrency payment gateways without managing private keys or blockchain infrastructure. The platform handles wallet creation, deposit address generation across multiple chains, automatic fund consolidation, and transaction monitoring through a unified SDK.

Architecture Overview

The payment flow consists of six steps:

  1. Applications create wallets per user via the Fystack SDK
  2. Fystack generates deposit addresses across Ethereum, Solana, Tron, and Bitcoin
  3. Users deposit funds to their assigned addresses
  4. Sweep tasks automatically consolidate funds into a central hot wallet when thresholds are met
  5. Consolidated funds reach the hot wallet for settlement
  6. Withdrawals and payouts distribute funds to merchants or external addresses

Build a crypto payment gateway with Fystack wallet infrastructure

The system uses Hyper wallets (HD-derived for high-volume user wallets) and MPC wallets (threshold signatures for treasury storage), eliminating direct private key exposure.

Prerequisites

  • Node.js 22 or later
  • Fystack account with API credentials
  • Workspace ID from the Fystack dashboard

Copy workspaceID from API Key section
Copy workspaceID from API Key section

Setup Steps

Step 1: Install and Configure SDK

npm install @fystack/sdk
Enter fullscreen mode Exit fullscreen mode

Create a .env file with credentials:

FYSTACK_API_KEY=your-api-key
FYSTACK_API_SECRET=your-api-secret
FYSTACK_WORKSPACE_ID=your-workspace-uuid
HOT_WALLET_ID=your-hot-wallet-uuid
Enter fullscreen mode Exit fullscreen mode

Initialize the SDK:

import {
  FystackSDK,
  Environment,
  WalletType,
  WalletPurpose,
  AddressType,
} from "@fystack/sdk";

const sdk = new FystackSDK({
  credentials: {
    apiKey: process.env.FYSTACK_API_KEY!,
    apiSecret: process.env.FYSTACK_API_SECRET!,
  },
  environment: Environment.Production,
  workspaceId: process.env.FYSTACK_WORKSPACE_ID!
});
Enter fullscreen mode Exit fullscreen mode

Step 2: Create a Hot Wallet

const hotWallet = await sdk.createWallet(
  {
    name: "Treasury Hot Wallet",
    walletType: WalletType.MPC,
    walletPurpose: WalletPurpose.General,
  },
  true // Wait for creation to complete
);

console.log("Hot wallet ID:", hotWallet.wallet_id);
Enter fullscreen mode Exit fullscreen mode

Create a wallet through Fystack portal
Create a wallet through Fystack portal

MPC wallet creation is asynchronous; the distributed key generation protocol requires 10-30 seconds across multiple nodes.

Step 3: Create a Sweep Task

Visit the Fystack portal → Automation → Add sweep task. Configure:

  • Task name
  • Destination wallet (the MPC hot wallet)
  • Minimum trigger value in USD

Create a sweep task
Create a sweep task

The sweep task ID appears as the last segment of the URL.

Step 4: Create Per-User Wallets

async function createUserWallet(userId: string) {
  const wallet = await sdk.createWallet(
    {
      name: `user_${userId}`,
      walletType: WalletType.Hyper,
      walletPurpose: WalletPurpose.OneTimeUse,
      sweepTaskID: '6bdc8e47-dc1a-4416-9158-b99cdc23205a'
    },
    true
  );

  return wallet;
}
Enter fullscreen mode Exit fullscreen mode

OneTimeUse wallets attempt to empty completely during sweeps rather than preserving dust amounts for future gas fees, preventing stranded balances across thousands of deposit wallets.

Step 5: Get Multi-Chain Deposit Addresses

async function getDepositAddresses(walletId: string) {
  const evm = await sdk.getDepositAddress(walletId, AddressType.Evm);
  const sol = await sdk.getDepositAddress(walletId, AddressType.Solana);

  return { address, qr_code };
}
Enter fullscreen mode Exit fullscreen mode

Supported chains by address type:

Type Chains Format
evm Ethereum, Polygon, Arbitrum, Optimism, Base 0x...
sol Solana Base58 public key
tron Tron T...
btc Bitcoin bc1q...

Display endpoint example:

app.get("/api/payment/:userId/address", async (req, res) => {
  const { userId } = req.params;
  const { chain } = req.query;

  const wallet = await getOrCreateWallet(userId);
  const addressType = chain === "solana" ? AddressType.Solana : AddressType.Evm;
  const deposit = await sdk.getDepositAddress(wallet.wallet_id, addressType);

  res.json({
    address: deposit.address,
    qrCode: deposit.qr_code,
    chain,
  });
});
Enter fullscreen mode Exit fullscreen mode

Step 6: Listen for Deposits with Webhooks

Fystack sends webhook events for transaction state changes:

Event Trigger Purpose
deposit.pending On-chain detection, unconfirmed Show pending UI
deposit.confirmed Blockchain confirmation Credit account
withdrawal.pending Withdrawal request created Update records
withdrawal.confirmed Transaction confirmed Mark complete
withdrawal.failed Transaction failed Alert team

Verify webhook signatures using Ed25519:

import crypto from "crypto";

const { public_key } = await sdk.getWebhookPublicKey(
  process.env.FYSTACK_WORKSPACE_ID!
);

app.post("/webhook/fystack", async (req, res) => {
  const signature = req.headers["x-webhook-signature"] as string;
  const event = req.headers["x-webhook-event"] as string;
  const payload = req.body;

  const canonical = JSON.stringify(sortKeysDeep(payload));

  const isValid = crypto.verify(
    null,
    Buffer.from(canonical),
    {
      key: Buffer.from(public_key, "base64"),
      format: "der",
      type: "spki",
    },
    Buffer.from(signature, "base64")
  );

  if (!isValid) {
    return res.status(401).json({ error: "Invalid signature" });
  }

  switch (event) {
    case "deposit.confirmed":
      await handleDepositConfirmed(payload);
      break;
    case "withdrawal.confirmed":
      await handleWithdrawalConfirmed(payload);
      break;
    case "withdrawal.failed":
      await handleWithdrawalFailed(payload);
      break;
  }

  res.status(200).json({ received: true });
});

function sortKeysDeep(obj: any): any {
  if (Array.isArray(obj)) return obj.map(sortKeysDeep);
  if (obj && typeof obj === "object") {
    return Object.keys(obj)
      .sort()
      .reduce((acc: any, key) => {
        acc[key] = sortKeysDeep(obj[key]);
        return acc;
      }, {});
  }
  return obj;
}
Enter fullscreen mode Exit fullscreen mode

Summary

This part covers wallet infrastructure, deposit address generation, automatic consolidation via sweep tasks, and webhook-based deposit detection. Part 2 will explore withdrawals, treasury management, and production architecture patterns.

For questions about custody setup, visit the inquiry form or join the Telegram community.

Top comments (0)