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.

📜 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
PAUSED403 Forbidden
FROZEN403 Forbidden
BLACKLISTED403 Forbidden
SUSPENDED403 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 }.