> ## Documentation Index
> Fetch the complete documentation index at: https://docs.taprails.xyz/llms.txt
> Use this file to discover all available pages before exploring further.

# Security & Compliance

> Technical deep-dive into TapRails production-grade security architecture for POS device signing, atomic settlement, and compliance screening.

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

<Steps>
  <Step title="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.
  </Step>

  <Step title="Register the Device">
    Send the **hex-encoded public key** and a stable `device_uuid` to TapRails when onboarding the device:

    ```bash theme={null}
    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"
      }
    }
    ```
  </Step>

  <Step title="Sign Every Payment Process Request">
    For each call to `payments/process`, sign the **raw JSON body** before sending it:

    ```javascript theme={null}
    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
    });
    ```
  </Step>
</Steps>

<Warning>
  **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.
</Warning>

### 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}
```

| Parameter | Description                                                                                            |
| :-------- | :----------------------------------------------------------------------------------------------------- |
| `id`      | The unique payment ID from the backend (e.g. `pay_...`)                                                |
| `sig`     | **Hex-encoded** Ed25519 signature of the deterministic signing string                                  |
| `t`       | Unix timestamp (seconds) from the NFC payload — matches the `t` field signed in the native HCE payload |

<Warning>
  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.
</Warning>

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

<Note>
  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.
</Note>

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:

| Field            | Description                                |
| :--------------- | :----------------------------------------- |
| `id`             | Unique ledger entry ID                     |
| `type`           | `PAYMENT`, `DEBIT`, `CREDIT`, `WITHDRAWAL` |
| `amount`         | Amount in USDC (decimal string)            |
| `balance_before` | Merchant balance **before** this event     |
| `balance_after`  | Merchant balance **after** this event      |
| `payment_id`     | Linked payment ID (if applicable)          |
| `status`         | `COMPLETED`, `PENDING`, `FAILED`           |
| `description`    | Human-readable description                 |
| `created_at`     | ISO 8601 timestamp                         |

### Querying the Ledger

```bash theme={null}
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.

<Note>
  If a ledger entry creation fails, the **entire payment confirmation is rolled back**. Payments are never confirmed without a corresponding ledger record.
</Note>

***

## 🛡️ 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 Status | Payment Request Result |
| :-------------- | :--------------------- |
| `ACTIVE`        | ✅ Processed            |
| `PAUSED`        | ❌ `403 Forbidden`      |
| `FROZEN`        | ❌ `403 Forbidden`      |
| `BLACKLISTED`   | ❌ `403 Forbidden`      |
| `SUSPENDED`     | ❌ `403 Forbidden`      |

See [MerchantStatus](/api-reference/types/merchant-status) 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:**

```javascript theme={null}
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)
  );
}
```

<Warning>
  Always use `timingSafeEqual` (or equivalent) to prevent timing attacks. Never compare signatures with `===`.
</Warning>

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 }`.
