Skip to content

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 records

Before 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]
ArgumentDescriptionExample
HTTP methodThe method to use for the requestGET, POST
API pathThe path without the Base URL, including the query string when filtering results/asset-types?code=USDT&page=1&page_size=20
JSON request bodyA 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' | jq

This 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.

VariablePreparation step
USER_ACCOUNT_ID, PAYMENT_ACCOUNT_IDDesign payment accounts
ASSET_TYPE_ID, NETWORK_ID, ASSET_DECIMALSFund payment assets
PAYMENT_AMOUNT, PAYMENT_REQUEST_ID, PAYMENT_WITHDRAWAL_IDMove assets between accounts
TO_ADDRESS, WITHDRAW_AMOUNT, ASSET_ID, SETTLEMENT_REQUEST_ID, WITHDRAWAL_IDSettle to an external address
Ledger IDs and resource cursorsReconcile 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 ​

  1. Design payment accounts
  2. Fund payment assets
  3. Move assets between accounts
  4. Record payment results
  5. Settle to an external address
  6. Collect and recover funds
  7. Reconcile payment transactions
  8. 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.

OOLIN is a digital asset operations infrastructure product by BPMG.