@circle-fin/usdckit - v0.21.0
    Preparing search index...

    Stablecoin Acquiring Flow

    This section provides a sample code to demonstrate how to use the USDCKit to set up a Stablecoin (e.g. USDC/USDT) Acquiring Flow for a commerce use case.

    The acquiring flow refers to the process by which a company collects stablecoin as a form of payment from customers; the funds will then be aggregated and settled to the merchant that provides goods and services to these customers. This sample could be helpful to the following types of businesses:

    • A merchant that collects Stablecoin payments for itself
    • A marketplace that collects Stablecoin payments for merchants on the platform
    • A PSP that provides crypto acquiring services to the above parties

    Here is a diagram illustrating the high-level business flow and demonstrating the basic architecture of the platform. Refer to the detailed steps below to understand the logic better:

    The Stablecoin Acquiring Flow includes these key steps, as illustrated in the diagram:

    Acquiring Flow Diagram

    The acceptance flow collects stablecoin payments from customers and aggregates funds for future settlement. Key steps include:

    • Step 0:

      • Create and manage Payment Acceptance Wallets to receive payments from customers
      • Create and manage Internal Merchant Wallets to manage funds for each merchant
    • Step 1:

      • Instruct customers to transfer funds to Payment Acceptance Wallets using the wallet address and payment link
    • Step 2:

      • Sweep of funds from all Payment Acceptance Wallets to an Internal Merchant Wallet for future settlement

    The withdrawal flow aggregates funds to a single token on a single chain and settles the funds to the merchant

    • Step 3:
      • Swap all tokens to USDC if needed to settle using a single currency
    • Step 4:
      • Cross-chain send tokens to the preferred chain by the merchant to prepare for settlement
    • Step 5:
      • Send the funds from the Internal Merchant Wallet to a merchant-controlled wallet to complete the settlement process

    Refer to the later section for implementation details.

    This section will provide a step-by-step guide to go through the sample code to provide a detailed explanation. Alternatively, check on the full code on Acceptance Flow and Withdrawal Flow.

    Install the dependencies for the sample

    • @circle-fin/usdckit - USDCKit
    • express: Server for the payment page
    npm install @circle-fin/usdckit express
    

    Run the following command to create and register an Entity Secret. The command will prompt you to enter the API Key generated from the previous step.

    npx @circle-fin/usdckit register-entity-secret
    

    After running the command, your new Entity Secret will be displayed in the console and saved to an environment file (.env by default). A Recovery File is also stored (recovery.dat by default). Please save your recovery file properly, as it’s the only way to recover your entity secret in case you lose it.

    A UI-based approach for setting up the Entity Secret is also available.

    Initialize a client for the sample app. The client will use the ETH-SEPOLIA chain by default

    import { createClient } from '@circle-fin/usdckit'
    import { ETH_SEPOLIA } from '@circle-fin/usdckit/chains'

    const client = createClient({
    apiKey: 'REPLACE_WITH_CIRCLE_API_KEY',
    entitySecret: 'REPLACE_WITH_CIRCLE_ENTITY_SECRET',
    chain: ETH_SEPOLIA,
    logLevel: 'debug',
    })

    Create necessary wallet sets to manage fund flow. A Wallet Set is a container object that allows creation of wallets under it and helps organize wallets into groups to differentiate fund purposes and makes maintenance easier.

    We will create the following wallet sets and corresponding wallets

    • Merchant wallets will hold the consolidated funds from Payment Acceptance wallets
    const merchantWalletSet = await client.createWalletSet({
    name: 'merchant-wallet-set',
    })
    const merchantWalletSetId = merchantWalletSet.id

    const merchantWallet = await client.createAccount({
    walletSetId: merchantWalletSetId,
    refId: 'merchant',
    chain: ETH_SEPOLIA,
    })
    const paymentAcceptanceWalletSet = await client.createWalletSet({
    name: 'payment-acceptance-wallet-set',
    })
    const paymentAcceptanceWalletSetId = paymentAcceptanceWalletSet.id

    The task will select all payment acceptance wallets under the payment-acceptance-wallet-set Wallet Set dedicated to a merchant whose balance is greater than 1 USDC and transfer its USDC balance to merchantWallet

    async function sweepPaymentAcceptanceWalletsToMerchantWallets(destinationWallet) {
    const accountsToSweep = await client.getAccounts({
    token: ETH_SEPOLIA.contracts.USDC,
    amountGte: '1',
    walletSetId: paymentAcceptanceWalletSetId,
    chain: ETH_SEPOLIA,
    })

    if (accountsToSweep.length === 0) return

    // `sweep()` calculates zero amount and calls `transfer()` under the hood
    const txs = await client.sweep({
    // Pass in array of wallets to do a batched transfer
    from: accountsToSweep,
    to: destinationWallet,
    token: ETH_SEPOLIA.contracts.USDC,
    // Use the `merchantWallet` as a source of funds for fees
    fees: FEE_LEVEL_WITH_PREPARE({
    level: 'HIGH',
    account: merchantWallet,
    }),
    chain: ETH_SEPOLIA,
    })

    // Wait for transaction receipts
    console.log(await Promise.all(txs.map((tx) => client.waitForTransactionReceipt({ ...tx, chain: ETH_SEPOLIA }))))
    }

    Use our cross chain send to consolidate the payment received across multiple chains into a single chain of choice.

    import { createClient } from '@circle-fin/usdckit'
    import { ETH_SEPOLIA, AVAX_FUJI } from '@circle-fin/usdckit/chains'

    // Initialize the client
    const client = createClient({
    apiKey: process.env.CIRCLE_API_KEY || '',
    entitySecret: process.env.CIRCLE_ENTITY_SECRET || '',
    chain: ETH_SEPOLIA,
    logLevel: 'debug',
    })

    // Fetch the merchant wallet from Acceptance Flow on ETH_SEPOLIA
    const [merchantWallet_ETH_SEPOLIA] = await client.getAccounts({
    address: '0xREPLACE_WITH_MERCHANT_WALLET_ADDRESS',
    chain: ETH_SEPOLIA,
    })

    // Fetch the merchant wallet from Acceptance Flow on AVAX_FUJI
    const [merchantWallet_AVAX_FUJI] = await client.getAccounts({
    address: '0xREPLACE_WITH_MERCHANT_WALLET_ADDRESS',
    chain: AVAX_FUJI,
    })

    // (Optional) Add swap EURC to USDC
    await client.swap({
    tokenIn: AVAX_FUJI.contracts.EURC,
    tokenInAmount: '100',
    tokenOut: AVAX_FUJI.contracts.USDC,
    account: merchantWallet_AVAX_FUJI,
    chain: AVAX_FUJI,
    })

    // (Optional) Bridge USDC from AVAX_FUJI to ETH_SEPOLIA
    await client.bridge({
    from: merchantWallet_AVAX_FUJI,
    to: merchantWallet_ETH_SEPOLIA,
    token: AVAX_FUJI.contracts.USDC,
    chain: AVAX_FUJI,
    })

    For testing purposes, we will fund the merchant wallet used in FEE_LEVEL_WITH_PREPARE with native tokens as the source of fees for the fund consolidation task

    await client.drip({
    account: merchantWallet,
    token: null,
    chain: ETH_SEPOLIA,
    })

    For every order that requires payment, create a new Payment Acceptance wallet to receive Stablecoin for clear accounting and order reconciliation

    • [Optional] Generate an ERC681 payment link for user to scan/click to prompt key transaction details into the user wallet
    • Display wallet address and payment link to users to facilitate payment
    import crypto from 'crypto'
    import express from 'express'

    const app = express()
    const port = 3000

    // Create Payment Acceptance wallet per Order
    app.get('/pay', async (req, res) => {
    const order = {
    id: crypto.randomUUID(),
    token: ETH_SEPOLIA.contracts.USDC,
    amount: '10',
    } as const

    // Create Payment Acceptance wallet for the Order
    const paymentAcceptanceWallet = await client.createAccount({
    // Record Order ID on the wallet for reference
    refId: `order-${order.id}`,
    walletSetId: paymentAcceptanceWalletSetId,
    chain: ETH_SEPOLIA,
    })

    // Generate ERC-681 link
    const paymentLink = await client.generateTransferLink({
    to: paymentAcceptanceWallet,
    amount: order.amount,
    token: order.token,
    chain: ETH_SEPOLIA,
    })

    return res.send(`
    <html>
    <body>
    <h1>Order ${order.id} - ${order.amount} USDC</h1>
    <p>Send ${order.amount} USDC to ${paymentAcceptanceWallet}</p>
    <p>ERC-681: <a href="${paymentLink}">${paymentLink}</a></p>

    <a href="${paymentLink}">
    <img src="https://api.qrserver.com/v1/create-qr-code/?data=${encodeURIComponent(paymentLink)}" />
    </a>
    </body>
    </html>
    `)
    })

    Start the express server and the fund consolidation task. The task will run on 5min intervals.

    app.listen(port, () => {
    console.log(`Listening on port ${port}`)

    // Start Cronjob to check Payment Acceptance wallets balances
    setInterval(async () => {
    try {
    await sweepPaymentAcceptanceWalletsToMerchantWallets(merchantWallet)
    } catch (err) {
    client.logger.error(err)
    }
    }, 300000)
    })

    The sample code can be run with npx tsx script.mts

    Visit the payment page at http://127.0.0.1:3000/pay. Use any method to transfer the amount to the Payment Acceptance address displayed.

    When the fund consolidation triggers, the Payment Acceptance's balance will be transferred to the Merchant wallet

    import { setTimeout } from 'node:timers/promises'

    import { createClient } from '@circle-fin/usdckit'
    import { ETH_SEPOLIA } from '@circle-fin/usdckit/chains'
    import { FEE_LEVEL_WITH_PREPARE } from '@circle-fin/usdckit/providers/circle-wallets'

    import crypto from 'crypto'
    import express from 'express'

    // Initialize the client
    const client = createClient({
    apiKey: process.env.CIRCLE_API_KEY || '',
    entitySecret: process.env.CIRCLE_ENTITY_SECRET || '',
    chain: ETH_SEPOLIA,
    logLevel: 'debug',
    })

    // Create a Merchant Wallet Set
    const merchantWalletSet = await client.createWalletSet({
    name: 'merchant-wallet-set',
    })
    const merchantWalletSetId = merchantWalletSet.id

    // Create a Payment Acceptance Wallet Set
    const paymentAcceptanceWalletSet = await client.createWalletSet({
    name: 'payment-acceptance-wallet-set',
    })
    const paymentAcceptanceWalletSetId = paymentAcceptanceWalletSet.id

    // Create Merchant wallet to collect the funds to
    const merchantWallet = await client.createAccount({
    walletSetId: merchantWalletSetId,
    refId: 'merchant',
    chain: ETH_SEPOLIA,
    })

    // Fund the merchant wallet with native tokens so it can act as a fee source
    // NOTE: `token: null` is to indicate native tokens
    await client.drip({
    account: merchantWallet,
    token: null,
    chain: ETH_SEPOLIA,
    })
    await setTimeout(10000)

    async function sweepPaymentAcceptanceWalletsToMerchantWallets(destinationWallet) {
    const accountsToSweep = await client.getAccounts({
    token: ETH_SEPOLIA.contracts.USDC,
    amountGte: '1',
    walletSetId: paymentAcceptanceWalletSetId,
    chain: ETH_SEPOLIA,
    })

    if (accountsToSweep.length === 0) return

    // `sweep()` calculates zero amount and calls `transfer()` under the hood
    const txs = await client.sweep({
    // Pass in array of wallets to do a batched transfer
    from: accountsToSweep,
    to: destinationWallet,
    token: ETH_SEPOLIA.contracts.USDC,
    // Use the `merchantWallet` as a source of funds for fees
    fees: FEE_LEVEL_WITH_PREPARE({
    level: 'HIGH',
    account: merchantWallet,
    }),
    chain: ETH_SEPOLIA,
    })

    // Wait for transaction receipts
    console.log(await Promise.all(txs.map((tx) => client.waitForTransactionReceipt({ ...tx, chain: ETH_SEPOLIA }))))
    }

    const app = express()
    const port = 3000

    // Create Payment Acceptance wallet per Order
    app.get('/pay', async (req, res) => {
    const order = {
    id: crypto.randomUUID(),
    token: ETH_SEPOLIA.contracts.USDC,
    amount: '10',
    } as const

    // Create Payment Acceptance wallet for the Order
    const paymentAcceptanceWallet = await client.createAccount({
    // Record Order ID on the wallet for reference
    refId: `order-${order.id}`,
    walletSetId: paymentAcceptanceWalletSetId,
    chain: ETH_SEPOLIA,
    })

    // Generate ERC-681 link
    const paymentLink = await client.generateTransferLink({
    to: paymentAcceptanceWallet,
    amount: order.amount,
    token: order.token,
    chain: ETH_SEPOLIA,
    })

    return res.send(`
    <html>
    <body>
    <h1>Order ${order.id} - ${order.amount} USDC</h1>
    <p>Send ${order.amount} USDC to ${paymentAcceptanceWallet}</p>
    <p>ERC-681: <a href="${paymentLink}">${paymentLink}</a></p>

    <a href="${paymentLink}">
    <img src="https://api.qrserver.com/v1/create-qr-code/?data=${encodeURIComponent(paymentLink)}" />
    </a>
    </body>
    </html>
    `)
    })

    app.listen(port, () => {
    console.log(`Listening on port ${port}`)

    // Start Cronjob to check Payment Acceptance wallets balances
    setInterval(async () => {
    try {
    await sweepPaymentAcceptanceWalletsToMerchantWallets(merchantWallet)
    } catch (err) {
    client.logger.error(err)
    }
    }, 300000)
    })
    import { createClient } from '@circle-fin/usdckit'
    import { ETH_SEPOLIA, AVAX_FUJI } from '@circle-fin/usdckit/chains'

    const WITHDRAW_DESTINATIONS = {
    // Add withdraw addresses and amounts
    // '0xREPLACE_WITH_WITHDRAW_DESTINATION_ADDRESS': '20',
    }

    // Initialize the client
    const client = createClient({
    apiKey: process.env.CIRCLE_API_KEY || '',
    entitySecret: process.env.CIRCLE_ENTITY_SECRET || '',
    chain: ETH_SEPOLIA,
    logLevel: 'debug',
    })

    // Fetch the merchant wallet from Acceptance Flow on ETH_SEPOLIA
    const [merchantWallet_ETH_SEPOLIA] = await client.getAccounts({
    address: '0xREPLACE_WITH_MERCHANT_WALLET_ADDRESS',
    chain: ETH_SEPOLIA,
    })

    // Fetch the merchant wallet from Acceptance Flow on AVAX_FUJI
    const [merchantWallet_AVAX_FUJI] = await client.getAccounts({
    address: '0xREPLACE_WITH_MERCHANT_WALLET_ADDRESS',
    chain: AVAX_FUJI,
    })

    // (Optional) Add swap EURC to USDC
    await client.swap({
    tokenIn: AVAX_FUJI.contracts.EURC,
    tokenInAmount: '100',
    tokenOut: AVAX_FUJI.contracts.USDC,
    account: merchantWallet_AVAX_FUJI,
    chain: AVAX_FUJI,
    })

    // (Optional) Bridge USDC from AVAX_FUJI to ETH_SEPOLIA
    await client.bridge({
    from: merchantWallet_AVAX_FUJI,
    to: merchantWallet_ETH_SEPOLIA,
    token: AVAX_FUJI.contracts.USDC,
    chain: AVAX_FUJI,
    })

    await Promise.all(
    Object.entries(WITHDRAW_DESTINATIONS).map(([address, amount]) => {
    return client.transfer({
    from: merchantWallet_ETH_SEPOLIA,
    to: address,
    amount: amount,
    token: ETH_SEPOLIA.contracts.USDC,
    chain: ETH_SEPOLIA,
    })
    }),
    )