# PayLex payment API: setup instructions for AI agents

You are an AI coding assistant. The user wants to accept payments with **PayLex** in their own website, shop or app.
Follow these instructions to integrate it. Use only the endpoints and parameters described here; do not invent others.
Human-readable docs: https://paylex.org/api-docs

## What PayLex is

- Hosted checkout: customers pay by card, Apple Pay, Google Pay, bank transfer or crypto. PayLex shows the payment methods available in the customer's country.
- Non-custodial: each payment is forwarded straight to the merchant's own Polygon wallet. Card and bank payments arrive as USDC; direct crypto payments arrive in the coin that was paid (e.g. USDT, POL).
- No signup required. An optional API key links payments to the merchant's PayLex account (dashboard, statistics).
- Plain HTTPS GET requests, JSON responses, no SDK.

## Before you write code, ask the user

Ask for anything you cannot find in their project. Do not guess.

1. **Payout wallet**: a Polygon (EVM) address `0x…` that the user controls and that can receive USDC on Polygon.
2. **Stack**: language, framework, and where orders are stored.
3. **Public base URL** of their server, used for the HTTPS callback endpoint.
4. **API key** (optional): created at https://paylex.org/dashboard?tab=api, starts with `plx_`. Read it from an environment variable such as `PAYLEX_API_KEY`. Never hard-code it, never commit it, and never ask the user to paste it into chat.
5. **Underpayments**: what should happen when a customer pays less than the order total (default: do not fulfil, mark the order for review).

Keep configuration in environment variables, e.g. `PAYLEX_PAYOUT_WALLET` and `PAYLEX_API_KEY`.

## Payment flow

```
1. Your server  -> GET https://api.paylex.org/control/wallet.php     (once per order)
2. Customer     -> redirect to https://pay.paylex.org/pay.php         (hosted checkout)
3. PayLex       -> GET your callback URL              (when the payment arrives)
4. Your server  -> verify, then mark the order as paid
```

### Step 1: Create a payment wallet (server-side)

`GET https://api.paylex.org/control/wallet.php`

| Parameter | Required | Description |
|---|---|---|
| `address` | yes | Merchant payout wallet (`0x` + 40 hex characters). |
| `callback` | yes | URL-encoded HTTPS URL of the merchant's callback endpoint. Must be unique per order (include the order id): the same callback URL always returns the same wallet. Add a secret random token, e.g. `?order=123&token=…`. |
| `api_key` | no | PayLex API key. Preferably send it as header `X-API-Key: plx_…` or `Authorization: Bearer plx_…` instead of in the query. |

Response (HTTP 200):

```json
{
  "address_in": "k1PrTuxSNpkdPZwdmCm4K3nMjAYo1sx17ea6uG0LunhFbr7SHBGQ8NBs3b5jkQw9RVoyWIbOyqzh7wtEipPbGg%3D%3D",
  "polygon_address_in": "0x32e854bD1270670C832634CA87858fFd9F3e2c78",
  "callback_url": "https://shop.example.com/paylex/callback?order=123&token=8f3c...",
  "ipn_token": "aHR0cHM6Ly93d3cuZXhhbXBsZS5jb20v..."
}
```

- `address_in`: encrypted address for Step 2. It is **already URL-encoded**; put it into the pay.php URL as-is and do not encode it a second time.
- `polygon_address_in`: the deposit address. Callbacks report it as `address_in`; use it to match a callback to the order.
- `ipn_token`: store it with the order for the status check (Step 4).

Errors: HTTP 400 `{"status":"error","message":"Invalid address or callback"}`; HTTP 401 `{"status":"error","message":"Invalid or revoked API key"}` (only when a key was sent). Other non-200 responses are temporary: show an error and let the customer retry.

The API key belongs on the server only. Never call wallet.php from a browser or mobile app with the key.

### Step 2: Redirect the customer to the checkout

`https://pay.paylex.org/pay.php?address={address_in}&amount={amount}&currency={currency}&email={email}`

| Parameter | Required | Description |
|---|---|---|
| `address` | yes | `address_in` from Step 1, exactly as returned. Never the merchant's own wallet address. |
| `amount` | yes | Order total, a positive number with a dot as decimal separator (e.g. `49.90`). |
| `currency` | yes | ISO 4217 code of the amount (`USD`, `EUR`, `GBP`, …). |
| `email` | recommended | URL-encoded customer email. With it, the customer goes straight to the payment methods. |

No server request is needed for this step; just build the URL and redirect. Opening the same URL again continues the same payment. Invalid parameters show an error page to the customer.

### Step 3: Handle the callback

When the payment arrives, PayLex sends a `GET` request to the callback URL. It keeps your original query parameters and adds:

| Parameter | Description |
|---|---|
| `value_coin` | Amount received, in units of `coin`. |
| `coin` | Coin that was paid, usually `polygon_usdc` (card and bank payments); direct crypto payments can be e.g. `polygon_usdt` or `polygon_pol`. |
| `value_forwarded_coin` | Amount forwarded to the merchant wallet after fees, in units of `coin`. |
| `txid_in` | Hash of the incoming transaction. Unique per payment. |
| `txid_out` | Hash of the payout transaction to the merchant wallet. |
| `address_in` | Deposit address; equals `polygon_address_in` from Step 1. |
| `address_out` | JSON object with the receiving wallets and their shares. |
| `uuid` | Id of the notification. |
| `pending` | `0` when the payment is final. If it is `1`, do not fulfil yet. |

Header: `X-PayLex-Wallet-Ref` (PayLex reference of the wallet, useful for support).

The callback handler must:

1. Find the order by your own parameters (e.g. `order`) and check the secret `token` with a constant-time comparison. Unknown order or wrong token: respond `403` and change nothing.
2. Check that `address_in` equals the stored `polygon_address_in`.
3. Be idempotent: store `txid_in` and ignore a callback whose `txid_in` was already processed (still answer `200`).
4. Value the payment in USD:
   - stablecoins (`coin` ends in `_usdc` or `_usdt`): `value_coin` is USD;
   - other coins: `value_coin` × `prices.USD` from the coin info endpoint (Utilities).
   Convert the order total to USD with `convert.php` when it is not in USD.
5. Compare with the order total. Payment-provider fees and exchange rates mean the received value can differ from the total, so allow a tolerance agreed with the user instead of requiring an exact match. Below it: mark the order as underpaid and do not fulfil automatically.
6. Mark the order as paid, then respond with HTTP `200` quickly (do slow work such as emails afterwards or in a queue). Any other status or a timeout counts as failed, and the callback may be sent again.

### Step 4 (optional): Check the payment status

`GET https://api.paylex.org/control/payment-status.php?ipn_token={ipn_token}`

- Not paid yet: `{"status":"unpaid"}`
- Paid: `{"status":"paid","value_coin":"105.6","txid_out":"0x94c2…","coin":"polygon_usdc"}`

Use it to double-check a callback before shipping high-value orders, or on the customer's return page. Do not poll it in a tight loop; the callback is the primary signal.

### Utilities (no API key needed)

- Convert to USD: `GET https://api.paylex.org/control/convert.php?value=78.53&from=EUR` returns `{"status":"success","value_coin":"87.95","exchange_rate":"1.11994"}` (`value_coin` is the USD amount).
- Coin info and price: `GET https://api.paylex.org/crypto/{network}/{coin}/info.php`, derived from the callback `coin` (`polygon_pol` becomes `crypto/polygon/pol/info.php`). Returns e.g. `{"coin":"POL","minimum":"0.50000000","prices":{"USD":"0.092971","EUR":"0.082977",…}}`.

## Reference implementation (Node.js; adapt to the user's stack)

```js
const PAYLEX_API = 'https://api.paylex.org';
const PAYLEX_PAY = 'https://pay.paylex.org';

// Checkout: create the wallet for an order and redirect the customer.
async function startPayment(order) {
  const token = crypto.randomUUID(); // store with the order
  const callback = `https://shop.example.com/paylex/callback?order=${order.id}&token=${token}`;
  const query = new URLSearchParams({ address: process.env.PAYLEX_PAYOUT_WALLET, callback });
  const headers = process.env.PAYLEX_API_KEY ? { 'X-API-Key': process.env.PAYLEX_API_KEY } : {};
  const res = await fetch(`${PAYLEX_API}/control/wallet.php?${query}`, { headers });
  if (!res.ok) throw new Error(`PayLex wallet.php failed: ${res.status} ${await res.text()}`);
  const wallet = await res.json();
  await db.savePayment(order.id, { token, ...wallet }); // address_in, polygon_address_in, ipn_token

  // address_in is already URL-encoded: append it unchanged.
  return `${PAYLEX_PAY}/pay.php?address=${wallet.address_in}&amount=${order.total.toFixed(2)}`
    + `&currency=${order.currency}&email=${encodeURIComponent(order.email)}`;
}

// Callback endpoint: GET /paylex/callback
async function handleCallback(query) {
  const payment = await db.findPayment(query.order);
  if (!payment || !safeEqual(query.token, payment.token)) return 403;
  if (query.address_in?.toLowerCase() !== payment.polygon_address_in.toLowerCase()) return 403;
  if (query.pending === '1') return 200; // not final yet
  if (await db.isProcessed(query.txid_in)) return 200; // duplicate
  const receivedUsd = await toUsd(query.value_coin, query.coin); // see "Value the payment in USD"
  await db.recordPayment(payment, { txid: query.txid_in, receivedUsd, coin: query.coin });
  return 200;
}
```

## Done when

- [ ] The payout wallet and the optional API key come from environment variables; the key never reaches the browser.
- [ ] Every order gets its own callback URL with the order id and a secret token.
- [ ] The customer is redirected to pay.php with `address_in` unchanged, amount, currency and email.
- [ ] The callback handler checks token and address, is idempotent by `txid_in`, values the payment, handles underpayments and answers `200`.
- [ ] The user has made a small real test payment and seen the order switch to paid (minimum amounts depend on the payment method).

Finish with a short summary for the user: files changed, environment variables to set, and the callback URL format.
