English
Digital Payment Tutorials
Build an offline payment proof of concept on Ethereum Sepolia with USDT. The customer pays from a USER account into a merchant BUSINESS account, and the merchant later settles collected funds to an external address.
Digital Payment is an integration scenario composed from Wallet API capabilities. There is no separate /payments API.
Run this example on a testnet. Keep production mainnets and test assets separate; see Supported Networks for network availability.
Customer funding→Internal payment→Merchant balance→External settlement→Reconciliation
What you will build
text
Discover the USDT asset and Sepolia network
→ create a customer account and merchant payment account
→ issue a customer deposit address and fund it
→ transfer USDT internally as a payment
→ recover and confirm the payment result
→ settle merchant funds to an external address
→ reconcile both accounts from ledger recordsBefore you start
Prepare Node.js, curl, openssl, and jq, along with the Base URL, API Key, and HMAC key issued by BPMG.
bash
export WALLET_BASE_URL='https://{environment-api-host}'
export WALLET_API_KEY='{issued-api-key}'
export WALLET_HMAC_KEY='{issued-hmac-key}'Do not commit credentials or print them in application logs. Synchronize the calling server clock with NTP because signed request timestamps must be within the server tolerance. See HMAC Request Signing before running the examples.
wallet_request
wallet_request is a shell function defined in this tutorial to call the Wallet API. The function below uses the environment variables configured above to generate an HMAC-SHA256 signature and authentication headers, then sends the HTTP request with curl. The code examples in later steps assume this function is registered.
Use the following call format:
text
wallet_request <HTTP method> <API path> [JSON request body]| Argument | Description | Example |
|---|---|---|
| HTTP method | The method to use for the request | GET, POST |
| API path | The path without the Base URL, including the query string when filtering results | /asset-types?code=USDT&page=1&page_size=20 |
| JSON request body | A JSON string supplied only when the request requires a body | "$user_account_body" |
Register the function
Paste and run the entire code block below in the shell where you configured the environment variables. It creates a new timestamp, nonce, and signature for every request. Configure the environment variables and register the function again whenever you open a new terminal or shell.
bash
wallet_request() {
local method="$1"
local request_path="$2"
local body="${3-}"
local timestamp nonce signature
timestamp="$(node -e 'process.stdout.write(String(Date.now()))')"
nonce="$(node -e 'process.stdout.write(require("crypto").randomUUID())')"
signature="$(printf '%s\n\n%s\n\n%s\n\n%s\n\n%s' \
"$method" "$request_path" "$timestamp" "$nonce" "$body" \
| openssl dgst -sha256 -hmac "$WALLET_HMAC_KEY" -binary \
| openssl base64 -A)"
if [ -n "$body" ]; then
curl -sS -X "$method" "$WALLET_BASE_URL$request_path" \
-H "X-API-KEY: $WALLET_API_KEY" \
-H "X-TIMESTAMP: $timestamp" \
-H "X-NONCE: $nonce" \
-H "X-SIGNATURE: $signature" \
-H 'Content-Type: application/json' \
--data-raw "$body"
else
curl -sS -X "$method" "$WALLET_BASE_URL$request_path" \
-H "X-API-KEY: $WALLET_API_KEY" \
-H "X-TIMESTAMP: $timestamp" \
-H "X-NONCE: $nonce" \
-H "X-SIGNATURE: $signature"
fi
}Example calls
After registering the function, run this query in the same shell. | jq formats the API's JSON response for readability.
bash
wallet_request GET '/asset-types?code=USDT&page=1&page_size=20' | jqThis call sends a signed GET request to $WALLET_BASE_URL/asset-types?code=USDT&page=1&page_size=20. For a POST request, pass the JSON body as the third argument, as in wallet_request POST '/accounts' "$user_account_body". The next step, Design payment accounts, shows how to prepare user_account_body. The query string and body are signed exactly as supplied, so quote the path and body arguments.
This helper is for the tutorial. A production client must also validate responses, set timeouts, and implement safe bounded retries.
The examples use Sepolia USDT, but asset, network, and account IDs must always come from the target environment's API responses.
Running examples and checking responses
Each page uses variables saved in earlier steps.
| Variable | Preparation step |
|---|---|
USER_ACCOUNT_ID, PAYMENT_ACCOUNT_ID | Design payment accounts |
ASSET_TYPE_ID, NETWORK_ID, ASSET_DECIMALS | Fund payment assets |
PAYMENT_AMOUNT, PAYMENT_REQUEST_ID, PAYMENT_WITHDRAWAL_ID | Move assets between accounts |
TO_ADDRESS, WITHDRAW_AMOUNT, ASSET_ID, SETTLEMENT_REQUEST_ID, WITHDRAWAL_ID | Settle to an external address |
| Ledger IDs and resource cursors | Reconcile payment transactions |
curl -sS can print JSON even for HTTP errors; | jq only formats the response. Check every request's HTTP status and success before continuing. Never use null extracted from an error or empty list as an ID. Also verify the returned asset, network, and account match your intent.
Shell variables are temporary storage for following the tutorial. In production, persist account mappings and order/settlement request IDs in a database. Bind the business ID to client_request_id before sending a create request, then add the transaction ID after receiving the response. Do not generate another payment request ID in a new shell and submit the same order again.
Tutorial sequence
- Design payment accounts
- Fund payment assets
- Move assets between accounts
- Record payment results
- Settle to an external address
- Collect and recover funds
- Reconcile payment transactions
- Build the complete payment flow
IMPORTANT
The IDs, addresses, and amounts shown here are examples. Use values returned by your environment and a valid destination address for the selected network.