Skip to main content
TapRails is engineered for high-stakes, mainnet-ready financial settlement. This guide covers three security pillars: Cryptographic Integrity, Financial Auditability, and Regulatory Compliance.

πŸ” POS Device Signing (Ed25519)

Every production call to POST /api/v1/sdk/payments/process must include a cryptographic signature. This prevents spoofing β€” no request can be fabricated without access to the physical device’s private key.

How Signing Works

1

Generate a Key Pair on First Launch

When the POS app starts for the first time on a new device, generate an Ed25519 key pair and store the private key in the device’s hardware Keystore or Keychain. Never expose the private key outside the secure enclave.
2

Register the Device

Send the hex-encoded public key and a stable device_uuid to TapRails when onboarding the device:
POST /api/v1/sdk/devices/register
x-api-key: pk_live_your_key

{
  "merchant_id": "mch_xyz789",
  "device_uuid": "550e8400-e29b-41d4-a716-446655440000",
  "public_key": "0x<32_byte_hex_encoded_raw_public_key>",
  "metadata": {
    "model": "Android NFC Reader",
    "platform": "android",
    "os_version": "14",
    "app_version": "1.2.0"
  }
}
3

Sign Every Payment Process Request

For each call to payments/process, sign the raw JSON body before sending it:
const body = JSON.stringify({
  paymentId: "pay_abc123",
  txHash: "0x...",
  customerWallet: "0x..."
});

// Sign with the device's Ed25519 private key
// (Use the Android Keystore or iOS Keychain API in production)
const signatureBuffer = sign(privateKey, Buffer.from(body));
const signatureHex = "0x" + signatureBuffer.toString("hex");

await fetch("/api/v1/sdk/payments/process", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "x-api-key": apiKey,
    "X-Device-Signature": signatureHex,
    "X-Device-Uuid": deviceUuid,
    "idempotency-key": crypto.randomUUID()
  },
  body   // exact same string that was signed
});
Do not serialize the body twice. The server verifies the signature against the exact raw string that was sent. If you JSON.stringify() once for signing and once again for the request body, the strings may differ and verification will fail.

Verification (Server Side)

Our backend performs the following checks in sequence:
  1. X-Device-Signature and X-Device-Uuid headers are present.
  2. The device_uuid maps to an ACTIVE registered device for the payment’s merchant.
  3. The Ed25519 signature is verified against the stored public key using the raw request body.
  4. If any check fails β†’ 401 Unauthorized is returned. The payment is not processed.

πŸ”— NDEF Fallback URL Security

The Dual-Path Flow introduces a web fallback for customers without a native app. To prevent URL tampering, the fallback URL is signed with the same Ed25519 private key used for native HCE invoice signing. The backend verifies the signature using the merchant’s pre-registered device public key.

Signed URL Structure

https://pay.taprails.xyz/p?id={paymentId}&sig={signatureHex}&t={unixTimestamp}
ParameterDescription
idThe unique payment ID from the backend (e.g. pay_...)
sigHex-encoded Ed25519 signature of the deterministic signing string
tUnix timestamp (seconds) from the NFC payload β€” matches the t field signed in the native HCE payload
The signature is Hex-encoded, not Base64. Base64 contains URL-unsafe characters (+, /) that are silently corrupted by browsers and proxies. Always use Hex when embedding Ed25519 signatures in URLs.

Signing Format

The value signed is the same deterministic string used for the native NFC payload. This means one signing operation covers both paths β€” no extra signing step is required:
a:{amountMicroUsdc}|exp:{expiryUnixSeconds}|id:{paymentId}|m:{merchantWalletAddress}|t:{timestamp}|v:1
Fields are sorted alphabetically and pipe-separated. All values are their raw string representations (no quoting or escaping).

Backend Verification Steps

When a customer opens the fallback URL, the backend validates it in this order before returning any payment data:
  1. Parameter validation β€” sig and t are present and t is a valid integer.
  2. Time window check β€” The timestamp t must be no more than 35 minutes in the past. This covers the 30-minute payment window plus 5 minutes of clock skew grace. Older URLs are rejected as expired regardless of signature validity.
  3. Payment lookup β€” The payment record is fetched using id.
  4. Payload reconstruction β€” The backend reconstructs the exact stringToSign using the payment’s stored amount, expiresAt, merchantWallet, and the URL’s t parameter.
  5. Signature verification β€” The backend retrieves all ACTIVE registered devices for the merchant and verifies the sig against each device’s public key. A match on any device is sufficient.
  6. Auto-expiry β€” If the payment is PENDING but past expiresAt, it is atomically marked EXPIRED before the response is returned.
Multi-device support is built in. If a merchant operates multiple POS terminals, the signature will be verified against all active registered devices β€” whichever device signed the URL is accepted.
This architecture ensures that even in the web-based fallback path, the security guarantees of hardware-backed device signing remain intact β€” the private key never leaves the device’s secure enclave.

πŸ“œ Atomic Financial Ledger

TapRails maintains a double-entry bookkeeping ledger to ensure complete financial integrity. Not a single cent of USDC can be moved without a corresponding audit record.

Ledger Entry Structure

Every balance-changing event creates a MerchantTransaction record simultaneously with the payment update, inside a single atomic database transaction:
FieldDescription
idUnique ledger entry ID
typePAYMENT, DEBIT, CREDIT, WITHDRAWAL
amountAmount in USDC (decimal string)
balance_beforeMerchant balance before this event
balance_afterMerchant balance after this event
payment_idLinked payment ID (if applicable)
statusCOMPLETED, PENDING, FAILED
descriptionHuman-readable description
created_atISO 8601 timestamp

Querying the Ledger

GET /api/v1/management/merchants/{merchant_id}/transactions?limit=50
x-api-key: pk_live_your_key
Use this to perform point-in-time balance reconciliation β€” each entry gives you the merchant’s exact balance at that moment in time.
If a ledger entry creation fails, the entire payment confirmation is rolled back. Payments are never confirmed without a corresponding ledger record.

πŸ›‘οΈ Compliance & Sanction Screening

Every customer wallet is automatically screened for compliance before a payment is confirmed.

Screening Process

  1. Internal Blacklist: The wallet address is checked against TapRails’ BlacklistedWallet table.
  2. External Integration (optional): A hook is available to connect to 3rd-party providers like TRM Labs or Chainalysis for broader sanctions coverage.
  3. Rejection: If the wallet is flagged β†’ 403 Forbidden with error code COMPLIANCE_REJECTED is returned. No funds move.

Merchant Status Enforcement

Merchants that are not ACTIVE are blocked from receiving payments at the API level:
Merchant StatusPayment Request Result
ACTIVEβœ… Processed
PAUSED❌ 403 Forbidden
FROZEN❌ 403 Forbidden
BLACKLISTED❌ 403 Forbidden
SUSPENDED❌ 403 Forbidden
See MerchantStatus for the full status lifecycle.

⛓️ High-Availability RPC

TapRails prevents single-provider failures on the Base network by supporting a multi-provider failover rail:
  • Configuration: Set BASE_RPC_URL as a comma-separated list of endpoints.
    BASE_RPC_URL="https://mainnet.base.org,https://base-mainnet.g.alchemy.com/v2/KEY"
    
  • Automatic Failover: Built on Viem’s fallback transport. If the primary RPC is slow or down, the system cycles through backups automatically.
  • Strict Transfer Decoding: We decode ERC-20 Transfer event logs from the block data (not just transaction receipts) to confirm the exact amount arrived at the correct merchant wallet.

πŸ”” Webhook Reliability & Signature Verification

Automatic Retries

Failed webhook deliveries are retried with exponential backoff for up to 24 hours. You can also view logs and trigger manual retries via the dashboard.

Verifying Signatures

Every TapRails webhook payload includes a signature header:
x-taprails-signature: <hmac_sha256_hex>
Verify it on your server:
import crypto from 'crypto';

function verifyWebhook(rawBody, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signature)
  );
}
Always use timingSafeEqual (or equivalent) to prevent timing attacks. Never compare signatures with ===.
Your webhook secret is auto-generated when you first set a webhook URL and is available in the dashboard (visible once on generation). You can rotate it at any time via POST /api/v1/dashboard/webhooks/config with { "rollSecret": true }.