# TrezorPay integration

You are a senior backend engineer. Integrate TrezorPay crypto payments into
[YOUR APPLICATION], so customers can pay orders in [THE COINS YOU WANT, e.g.
USDT on Tron, USDC on Ethereum] and the money lands in the merchant's own
wallet. Two directions of traffic:

```
1. Your backend -> TrezorPay
   - YOU call public GET endpoints at https://api.trezorpay.com
   - No API key, no account, no request signing: the CALLBACK URL you send
     is the identity of a payment address

2. TrezorPay -> your backend
   - WE call your callback URL when a payment arrives and when it has been
     forwarded to the merchant's wallet
   - WE sign every callback with our RSA private key
   - YOU verify it with our public key and answer *ok*
```

TrezorPay is non-custodial: each payment is forwarded on-chain to the wallet
you choose, minus a 1.25% fee and the network cost. There is no balance to
withdraw. Your integration only has to create addresses, show them, and
record payments from verified callbacks.

A TEST SUITE is part of the deliverable, not an afterthought. The cases your
callback handler must pass are written out at the end of this prompt ("Prove
it locally"). Read them now and build them alongside the code.

## Configuration, not constants

Keep everything environment-specific in configuration:

```
TREZORPAY = {
  apiBaseUrl:      "https://api.trezorpay.com",
  callbackBaseUrl: env,                  // public HTTPS URL of YOUR server
  wallets: {                             // where the money goes, per network family
    evm:  env,                           // 0x… address (Ethereum, BNB Smart Chain, Polygon, Base, Arbitrum, Optimism, Avalanche)
    tron: env,                           // T… address
    btc:  env,                           // bc1… / 1… / 3… address (Bitcoin)
    ltc:  env,                           // ltc1… / L… / M… address (Litecoin)
    bch:  env,                           // bitcoincash:q… address (Bitcoin Cash)
    doge: env,                           // D… address (Dogecoin)
    sol:  env,                           // base58 address (Solana)
  },
  tickers: ["trc20/usdt", "erc20/usdc"], // the coins you accept
}
```

Never hardcode a wallet. Fetch the TrezorPay public key at boot (see
"Verifying callbacks") and cache it; do not fetch it per request.

## Conventions of the API

- Every endpoint is an HTTP GET with query parameters, and every path ENDS
  WITH A SLASH: `/trc20/usdt/create/`, not `/trc20/usdt/create`.
- Success is HTTP 200 with `"status": "success"`.
- Errors are HTTP 400 (404 for an unknown resource) with
  `{ "status": "error", "error": "<message>" }`. Route on the status, show or
  log the message, never parse it for logic.
- Amounts are JSON STRINGS ("10.00000000"). Parse them with a decimal type
  (BigDecimal, Decimal, bignumber, dinero), NEVER a float/double/JS number.
- Request URLs are limited to 8192 characters.
- Timeouts: give every call 15 seconds and retry idempotent GETs on network
  errors and 5xx with exponential backoff.

## Coins (the {ticker} path segment)

```
ticker           coin    network           minimum   confirmations
eth              ETH     Ethereum          0.0015    3
erc20/usdt       USDT    Ethereum          2         3
erc20/usdc       USDC    Ethereum          2         3
erc20/1inch      1INCH   Ethereum          75        3
erc20/arb        ARB     Ethereum          15        3
erc20/bnb        BNB     Ethereum          0.09      3
erc20/cbbtc      cbBTC   Ethereum          0.00009   3
erc20/dai        DAI     Ethereum          15        3
erc20/link       LINK    Ethereum          0.65      3
erc20/musd       mUSD    Ethereum          5         3
erc20/ondo       ONDO    Ethereum          10        3
erc20/pepe       PEPE    Ethereum          1000000   3
erc20/pol        POL     Ethereum          25        3
erc20/pyusd      PYUSD   Ethereum          2         3
erc20/shib       SHIB    Ethereum          1000000   3
erc20/trx        TRX     Ethereum          15        3
erc20/usd1       USD1    Ethereum          5         3
erc20/wxrp       wXRP    Ethereum          6         3
erc20/xaut       XAUt    Ethereum          0.0015    3
bep20/bnb        BNB     BNB Smart Chain   0.001     15
bep20/usdt       USDT    BNB Smart Chain   1         15
bep20/usdc       USDC    BNB Smart Chain   1         15
bep20/1inch      1INCH   BNB Smart Chain   1         15
bep20/ada        ADA     BNB Smart Chain   1         15
bep20/btcb       BTCB    BNB Smart Chain   0.00008   15
bep20/cake       CAKE    BNB Smart Chain   0.1       15
bep20/dai        DAI     BNB Smart Chain   1         15
bep20/doge       DOGE    BNB Smart Chain   10        15
bep20/eth        ETH     BNB Smart Chain   0.00075   15
bep20/ltc        LTC     BNB Smart Chain   0.002     15
bep20/shib       SHIB    BNB Smart Chain   40000     15
bep20/trx        TRX     BNB Smart Chain   5         15
bep20/usd1       USD1    BNB Smart Chain   1         15
bep20/xrp        XRP     BNB Smart Chain   2         15
polygon/pol      POL     Polygon           0.5       64
polygon/usdt     USDT    Polygon           0.5       64
polygon/usdc     USDC    Polygon           0.5       64
polygon/avax     AVAX    Polygon           0.04      64
polygon/usdc.e   USDC.e  Polygon           0.5       64
polygon/weth     WETH    Polygon           0.0005    64
base/eth         ETH     Base              0.0003    10
base/usdc        USDC    Base              3         10
base/cbbtc       cbBTC   Base              0.00003   10
base/cbltc       cbLTC   Base              0.05      10
base/dai         DAI     Base              3         10
base/usdt        USDT    Base              3         10
arbitrum/eth     ETH     Arbitrum          0.0001    20
arbitrum/usdt0   USD₮0   Arbitrum          1         20
arbitrum/usdc    USDC    Arbitrum          1         20
arbitrum/arb     ARB     Arbitrum          0.5       20
arbitrum/dai     DAI     Arbitrum          0.5       20
arbitrum/link    LINK    Arbitrum          0.05      20
arbitrum/pepe    PEPE    Arbitrum          100000    20
arbitrum/pyusd   PYUSD   Arbitrum          1         20
arbitrum/usdc.e  USDC.e  Arbitrum          1         20
optimism/eth     ETH     Optimism          0.0001    10
optimism/usdc    USDC    Optimism          1         10
optimism/usdc.e  USDC.e  Optimism          1         10
optimism/usdt    USDT    Optimism          1         10
optimism/usdt0   USD₮0   Optimism          1         10
optimism/dai     DAI     Optimism          0.5       10
optimism/link    LINK    Optimism          0.05      10
optimism/op      OP      Optimism          0.3       10
avax-c/avax      AVAX    Avalanche         0.01      3
avax-c/usdc      USDC    Avalanche         1         3
avax-c/usdc.e    USDC.e  Avalanche         1         3
avax-c/usdt      USDT    Avalanche         1         3
avax-c/wavax     WAVAX   Avalanche         0.01      3
avax-c/weth.e    WETH.e  Avalanche         0.001     3
trx              TRX     Tron              10        19
trc20/btc        BTC     Tron              0.00008   19
trc20/tusd       TUSD    Tron              5         19
trc20/usdd       USDD    Tron              5         19
trc20/usdt       USDT    Tron              10        19
btc              BTC     Bitcoin           0.00008   1
ltc              LTC     Litecoin          0.002     2
bch              BCH     Bitcoin Cash      0.0005    1
doge             DOGE    Dogecoin          10        6
sol/sol          SOL     Solana            0.004     1
sol/usdc         USDC    Solana            1         1
sol/usdt         USDT    Solana            1         1
sol/eurc         EURC    Solana            1         1
sol/wbtc         WBTC    Solana            0.000009  1
sol/cbbtc        cbBTC   Solana            0.000009  1
sol/weth         WETH    Solana            0.003     1
sol/trump        TRUMP   Solana            0.05      1
sol/hnt          HNT     Solana            2.5       1
sol/wusdt        wUSDT   Solana            1         1
```

Callbacks name the coin with an underscore (`trc20_usdt`, `erc20_usdc`,
`eth`). The live list, minimums and fees come from `GET /info/`: read
minimums from there at runtime instead of copying this table.

## 1. Create a payment address

```
GET /{ticker}/create/?callback=<url>&address=<wallet>[&pending=1]
```

Query parameters:

- `callback` (required): your callback URL, URL-encoded. It IS the identity:
  the same callback URL always returns the same `address_in`, and its query
  string comes back on every callback. Put your own identifiers in it, e.g.
  `https://shop.example/trezorpay/callback?order=42`.
- `address` (required): the merchant wallet, or a split
  `0.7@<wallet1>|0.3@<wallet2>` (up to 20 outputs, shares sum to 1).
- `pending=1`: also call back when the payment is first seen, before it
  confirms. Use it to show "payment detected" to the customer.
- `confirmations`: confirmations before the confirmed callback (raised to the
  network's safety floor; you rarely need it).
- `post=1`: callbacks as POST `application/x-www-form-urlencoded`.
- `json=1`: callbacks as POST `application/json`. Recommended.
- `multi_token=1`: the address accepts every supported token of its network
  (then trust `coin` in the callback, not the ticker you created with).
- `convert=1`: add fiat values to callbacks.

Response:

```json
{
  "status": "success",
  "address_in": "TN7Zbu4UwtKneYDJyDKXkSorVu95hsE3q4",
  "address_out": "TJzXZQhbhJB3dA2TmxmvyPxk2nKsmZ6yWf",
  "callback_url": "https://shop.example/trezorpay/callback?order=42",
  "minimum_transaction_coin": "10.00000000",
  "priority": "default"
}
```

Rules you must respect:

- The address is IMMUTABLE per callback URL: calling again with the same
  callback returns the ORIGINAL `address_in` and `address_out`, whatever
  else you send. To change the destination wallet, change the callback URL.
- Addresses never expire and can be paid any number of times. Persist the
  mapping (order or customer) -> (ticker, callback URL, address_in).
- Payments below `minimum_transaction_coin` are recorded but NEVER forwarded.
  Show the minimum to the customer.

## 2. Show the address

Show `address_in`, the exact amount to send and the coin + network (USDT on
Tron is not USDT on Ethereum: a payment on the wrong network is lost).
Optionally:

```
GET /{ticker}/qrcode/?address=<address_in>&value=<amount>&size=512
-> { "status": "success", "qr_code": "<base64 PNG>", "payment_uri": "..." }
```

To quote a fiat price in a coin:

```
GET /{ticker}/convert/?value=49.90&from=usd
-> { "status": "success", "value_coin": "49.91", "exchange_rate": "1.00018" }
```

Store the quoted coin amount with the order: that is what you compare against
when the payment arrives.

## 3. Receive callbacks

Per incoming payment, TrezorPay calls your callback URL:

- once with `pending=1` when the payment is seen (only if you asked with
  `pending=1`), and
- once with `pending=0` after it has been forwarded to the merchant wallet.

Transport: GET with every field in the query string (default), a POST form
body (`post=1`), or a POST JSON body (`json=1`). Your own query parameters
are always kept on the URL.

Fields (all strings):

```
uuid                   payment id; IDENTICAL on its pending and confirmed
                       callback. Your idempotency key.
address_in             the address that was paid
address_out            the merchant wallet (or the split)
txid_in                the customer's transaction
txid_out               our forwarding transaction (pending=0 only)
confirmations          confirmations at callback time
value_coin             amount the customer paid
value_forwarded_coin   amount sent to the merchant wallet (pending=0 only)
fee_coin               our fee (pending=0 only)
coin                   underscore ticker, e.g. trc20_usdt
price                  coin price in USD
pending                "1" or "0"
result                 "pending" or "sent"
value, value_forwarded, fee
                       legacy integers: IGNORE them
value_coin_convert, value_forwarded_coin_convert
                       with convert=1: JSON strings of fiat values,
                       e.g. {"USD":"49.90"}
```

Ignore fields you do not know: new fields may be added.

Money rule: `value_forwarded_coin = value_coin - fee_coin - network cost`.
Decide whether an order is paid on `value_coin` (what the customer sent),
never on `value_forwarded_coin`.

### Verifying callbacks (mandatory, before anything else)

Every callback carries the header `x-ca-signature`: base64 (standard, padded)
of an RSA-SHA256 PKCS#1 v1.5 signature.

- GET callback: the signed data is the FULL URL exactly as we requested it:
  scheme + host + path + query string, e.g.
  `https://shop.example/trezorpay/callback?order=42&uuid=…&pending=0…`
- POST callback: the signed data is the RAW BODY BYTES, before any parsing.

Public key (PEM), fetched once at boot:

```
GET /pubkey/ -> { "status": "success", "pubkey": "-----BEGIN PUBLIC KEY-----\n…" }
```

On every callback, BEFORE parsing or trusting any field:

1. Reject with 401 if `x-ca-signature` is missing.
2. Rebuild the signed data (full URL or raw body) and verify. Reject 401 if
   it fails.
3. Behind a reverse proxy, rebuild the URL as the CLIENT saw it (honour
   X-Forwarded-Proto / X-Forwarded-Host, keep the query string byte for
   byte, never re-encode it). This is the most common integration bug.
4. For POST, capture the raw body before your framework parses it.

Header names are case-insensitive.

### Acknowledging

Answer HTTP 200 with the exact body `*ok*` (no quotes, no JSON) within 30
seconds. Anything else is retried after 6, 12, 24 minutes and so on,
doubling, until the payment is 3 days old. So:

- answer `*ok*` for a valid callback you have ALREADY processed (replays are
  normal);
- answer 401 for a bad signature;
- never answer `*ok*` before your database write is committed;
- do slow work (emails, fulfilment) asynchronously after committing.

### Processing order (do it in exactly this order)

```
signature (401)
  -> parse fields
  -> look up the address/order from your callback query parameters and
     check address_in matches what you stored (else 400, log it)
  -> idempotency on (uuid, pending): if already recorded, answer *ok*
  -> record the payment
  -> pending=1: mark the order "payment detected" (never fulfil)
  -> pending=0: add value_coin to the order's received total; if the total
     reaches the expected amount, mark the order PAID and fulfil
  -> commit, then answer *ok*
```

## Idempotency & partial payments (read this twice)

- The idempotency key is `uuid` + `pending`, never the order and never the
  address. Enforce it with a database UNIQUE constraint on (uuid, pending),
  not an application read-then-write: callbacks and their retries can arrive
  concurrently.
- One address can receive several payments: each has its own uuid and its
  own pair of callbacks. Sum `value_coin` of confirmed (`pending=0`) payments
  per order to support customers who pay in several transfers.
- Underpayment: if the confirmed total is below the expected amount, keep the
  order open and show the remaining amount. Overpayment: mark paid and keep
  the surplus visible to the merchant (no automatic refund exists: funds are
  already in the merchant wallet).
- A pending callback may never be followed by a confirmed one (the
  transaction can be dropped). Never fulfil on `pending=1`.
- Retries are identical: a known uuid never arrives with different amounts.
- Keep records forever-ish: a retry can arrive up to 3 days later.

## Reading payment history

```
GET /{ticker}/logs/?callback=<the exact callback URL>
```

Returns the address and every payment with its last 10 callback attempts.
Use it in an admin "resync" action and to debug a callback that never
reached you. `result` is `pending` (confirming), `sent` (forwarded, your
server has not answered `*ok*` yet) or `done` (acknowledged).

## Errors you will see

```
You must provide a valid address and a callback         missing parameter
Callback URL malformed, please provide a valid URL      callback not http(s)
You must provide a valid address                        wallet invalid for the ticker
Maximum 20 output addresses                             split too long
Non multi_token address already created for another token
                                                        callback reused for another coin
Resource not found (404)                                unknown ticker path
Callback not found (404)                                /logs/ for an unknown callback
Coin not supported                                      bad currency in /convert/
Your request couldn't be processed, please try again later
                                                        temporary: retry with backoff
```

Surface 400s to the developer (configuration bugs); retry only network
errors, 5xx and the "try again later" message.

## Prove it locally: the callback test suite

Write these as real tests in your stack's framework, driving your callback
endpoint over REAL HTTP (signature verification, routing, body parsing,
your database and its unique constraints all in the path). Generate a test
RSA key pair, configure your app to trust its public key as "TrezorPay" in
tests, and sign requests exactly as described above. Run every case for each
transport you enabled (GET, and POST JSON if you use `json=1`).

Fixture: an order expecting 49.90 USDT with ticker `trc20/usdt`, its callback
URL `<base>/trezorpay/callback?order=<id>` and a stored `address_in`.

### signature (5)

- **sig.valid**: correctly signed confirmed callback -> 200 `*ok*`, order paid.
- **sig.missing**: no `x-ca-signature` header -> 401, nothing recorded.
- **sig.tampered**: sign, then change `value_coin` in the URL (or body) -> 401.
- **sig.wrongkey**: signed with another RSA key -> 401.
- **sig.proxy**: the request arrives through your proxy configuration
  (forwarded headers set) -> still verifies -> 200.

### flow (6)

- **flow.pending_then_confirmed**: `pending=1` (uuid U) -> 200, order
  "detected", not paid; `pending=0` (same U, value_coin 49.90) -> 200, order
  paid once.
- **flow.confirmed_only**: only `pending=0` arrives -> order paid.
- **flow.unknown_order**: valid signature, order id you do not have -> 400,
  nothing recorded.
- **flow.address_mismatch**: valid signature, `address_in` differs from the
  stored one -> 400, nothing recorded.
- **flow.underpaid**: confirmed 30.00 of 49.90 -> order stays open, remaining
  19.90 shown.
- **flow.two_payments**: confirmed 30.00 (uuid U1) then 19.90 (uuid U2) ->
  paid exactly when the second lands.

### idempotency (4)

- **idem.replay_confirmed**: the same confirmed callback twice -> both 200
  `*ok*`, recorded once, fulfilment triggered once.
- **idem.replay_pending**: the same pending callback twice -> recorded once.
- **idem.concurrent**: the same confirmed callback 10 times in parallel ->
  exactly one payment row (the unique constraint holds).
- **idem.ack_after_commit**: make the database write fail -> the response is
  NOT `*ok*` (5xx), so TrezorPay retries.

### money (3)

- **money.decimal**: value_coin "0.000001" and "123456789.123456" round-trip
  without float error.
- **money.overpaid**: confirmed 60.00 for 49.90 -> paid, surplus 10.10
  recorded.
- **money.paid_on_value_coin**: `value_forwarded_coin` below the expected
  amount but `value_coin` equal -> order paid (fees are not the customer's
  shortfall).

### client (3)

- **client.trailing_slash**: every API URL you build ends with "/".
- **client.amount_strings**: responses are parsed as decimals, never floats.
- **client.error_envelope**: a 400 `{"status":"error"}` from /create/ surfaces
  the message to the caller and is not retried; a 503 is retried with backoff.

## Deliverable

1. A small client module: createAddress, getQrCode, convert, getLogs, with
   configuration as above.
2. The callback endpoint with verification, ordering and idempotency as
   above.
3. Database migrations: payment addresses (order -> ticker, callback URL,
   address_in) and payments (unique uuid + pending, value_coin, txid_in,
   txid_out, coin, timestamps).
4. The test suite above, green.
5. A short README: environment variables, how to expose the callback URL in
   development (an HTTPS tunnel; localhost callbacks are refused), and how to
   resync an order from /logs/.

Docs: https://trezorpay.com/docs/ · OpenAPI: https://trezorpay.com/openapi.yaml
