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:
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:
The acceptance flow collects stablecoin payments from customers and aggregates funds for future settlement. Key steps include:
Step 0:
Step 1:
Step 2:
The withdrawal flow aggregates funds to a single token on a single chain and settles the funds to the merchant
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
- USDCKitexpress
: Server for the payment pagenpm 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-wallet-set
)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,
})
payment-acceptance-wallet-set
)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
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,
})
}),
)