# AGENTS.md: blockwallets payment integration

Rules for coding agents (Claude Code, Codex, Cursor, Copilot…) adding blockwallets crypto payments to a project. Save this file as `AGENTS.md` in the project root (Claude Code: add `@AGENTS.md` to `CLAUDE.md`). Talk to the user in their own language; many users are not programmers, so explain each change and what they need to do.

- Full docs (Markdown): https://blockwallets.io/llms-full.txt
- Human guide: https://blockwallets.io/docs/ai?lang=en
- Signature checker (runs in the browser): https://blockwallets.io/docs/sign?lang=en
- MCP server (search the docs, create test bills from your agent): https://blockwallets.io/docs/mcp?lang=en
- Every way to integrate (overview): https://blockwallets.io/docs.md?lang=en

Read the docs before writing code. Do not guess endpoints or fields from memory: only what the docs list is supported.

## What blockwallets is

A crypto payment gateway: customers pay in USDT, USDC, BTC, LTC and native coins (17 coins on 15 networks); the merchant is credited in US dollars. Stablecoins are 1:1 with USD; other coins are quoted at a rate locked for 30 minutes.

## Pick the integration

1. **Bills (default)**: create a bill at checkout, redirect to the hosted payment page, receive a signed webhook. `POST /v1/payment` → webhook `type: payment`.
2. **Static wallets**: a permanent deposit address per user, any amount, any time. `POST /v1/wallet` → webhook `type: wallet`.
3. **Payment links (no code)**: created in the merchant dashboard; just link or embed them. No API keys needed.

Ask the user which one fits when it is not obvious. When in doubt, use bills.

## The project already uses another gateway

Do not rewrite it: blockwallets has compatible APIs, so you change the base URL and credentials and keep the existing code. Read the guide for that gateway first and follow its list of differences.

| Existing integration | Change | New credentials (dashboard → Developers › Integration & keys) | Guide |
|---|---|---|---|
| Cryptomus | `https://api.cryptomus.com` → `https://blockwallets.io` | merchant + API key | https://blockwallets.io/docs/cryptomus.md?lang=en |
| NOWPayments | `https://api.nowpayments.io/v1` → `https://blockwallets.io/nowpayments/v1` | API key + IPN secret | https://blockwallets.io/docs/nowpayments.md?lang=en |
| Coinbase Commerce | SDK base URL → `https://blockwallets.io/coinbase/` | API key + webhook shared secret | https://blockwallets.io/docs/coinbase-commerce.md?lang=en |

For a new integration with no existing gateway, the docs recommend the native REST API (https://blockwallets.io/docs/api.md?lang=en): Bearer keys the user generates in the dashboard (Developers › Integration & keys), amounts in US cents, an idempotent `ref`, HMAC-SHA256 webhooks. If you use it, follow that page instead of the rules below. The rules and code below are for the Cryptomus-compatible API (merchant + API key, also generated in the dashboard): use it when the project already speaks Cryptomus or uses a Cryptomus SDK / plugin, or when the user prefers it.

## Rules

### Bills
1. Every endpoint is POST + JSON on https://blockwallets.io. Headers: merchant = the merchant UUID, sign = md5(base64(request body) + API key). Sign the exact JSON string you send; do not re-serialize after signing.
2. Create a bill with POST /v1/payment. Required: amount (a string with up to two decimals, e.g. "42.00"), currency ("USD"), order_id (my order number: letters, digits, - and _, up to 100 characters). Also pass url_callback (the webhook URL) and url_success (where the customer returns after paying). Redirect the customer to result.url.
3. Calling again with the same order_id returns the same bill, so retries are safe and never create a second bill.
4. Webhook signatures must be verified: parse the webhook JSON, drop the sign field, re-encode the rest the way PHP json_encode does (compact, non-ASCII kept, / escaped as \/), then md5(base64(encoded) + API key) must equal sign. Forgetting to escape / is the most common mistake.
5. Mark the order paid only when status is paid or paid_over, using payment_amount_usd. The same webhook can arrive more than once: deduplicate by uuid + status. Reply with a 2xx status once handled.
6. Keep the merchant ID and API key in environment variables (e.g. BW_MERCHANT, BW_API_KEY); never hard-code them or commit them. Add the domain of the webhook URL under “Website domains” on the dashboard’s “Developers › Integration & keys” page first.
7. Get everything working with the Test credentials first (testnets, no real money); switch to Live only after I confirm.

### Static wallets
1. Open an address with POST /v1/wallet: order_id (my user ID), currency (e.g. "USDT"), network (e.g. "tron", "bsc"), url_callback. The same user + network always returns the same address. result.address is the address; result.url is a hosted deposit page I can give to the user.
2. Every transfer triggers a webhook with type wallet and status paid. Each transfer has its own uuid: deduplicate by it and credit payment_amount_usd to that user. Transfers below the network minimum (usually $1, BTC $5) are not credited automatically.
3. Webhook signatures must be verified: parse the webhook JSON, drop the sign field, re-encode the rest the way PHP json_encode does (compact, non-ASCII kept, / escaped as \/), then md5(base64(encoded) + API key) must equal sign. Forgetting to escape / is the most common mistake.
4. Keep the merchant ID and API key in environment variables (e.g. BW_MERCHANT, BW_API_KEY); never hard-code them or commit them. Add the domain of the webhook URL under “Website domains” on the dashboard’s “Developers › Integration & keys” page first.
5. Test first with the Test credentials: POST /v1/test-webhook/wallet (with url_callback, which is required and must be on my registered website domains, plus currency, network and order_id) makes blockwallets send a signed sample webhook to my url_callback.

## Signing a request (Node.js, from the docs)

```js
import crypto from "node:crypto";

async function call(path, data) {
  const body = JSON.stringify(data);
  const sign = crypto.createHash("md5").update(Buffer.from(body).toString("base64") + process.env.BW_API_KEY).digest("hex");
  const res = await fetch("https://blockwallets.io" + path, {
    method: "POST",
    headers: { merchant: process.env.BW_MERCHANT, sign, "Content-Type": "application/json" },
    body,
  });
  return res.json();
}
```

## Verifying a webhook (from the docs)

```js
import crypto from "node:crypto";

function verify(rawBody, apiKey) {
  const { sign, ...data } = JSON.parse(rawBody);
  const json = JSON.stringify(data).replaceAll("/", "\\/");
  const expected = crypto.createHash("md5").update(Buffer.from(json).toString("base64") + apiKey).digest("hex");
  return typeof sign === "string" && sign.length === expected.length && crypto.timingSafeEqual(Buffer.from(sign), Buffer.from(expected));
}
```

```php
$data = json_decode(file_get_contents('php://input'), true);
$sign = $data['sign'] ?? '';
unset($data['sign']);
$expected = md5(base64_encode(json_encode($data, JSON_UNESCAPED_UNICODE)) . $apiKey);
if (!hash_equals($expected, $sign)) {
    http_response_code(401); exit;
}
```

```python
import base64, hashlib, hmac, json

def verify(raw_body: bytes, api_key: str) -> bool:
    data = json.loads(raw_body)
    sign = data.pop("sign", "")
    encoded = json.dumps(data, separators=(",", ":"), ensure_ascii=False).replace("/", "\\/")
    expected = hashlib.md5((base64.b64encode(encoded.encode()).decode() + api_key).encode()).hexdigest()
    return hmac.compare_digest(expected, sign)
```

## Common mistakes

- Re-serializing the request body after signing it (pretty-printing, key reordering, a framework re-encoding the object). Serialize once, sign that string, send that string. On a 401 for a bad signature, `debug.body_base64` in the response is the text the server signed: compare it with yours.
- Not escaping `/` as `\/` when re-encoding a webhook for verification. Python also needs `separators=(",", ":")` and `ensure_ascii=False`.
- Verifying against a parsed-and-re-serialized body from the framework instead of the raw request body.
- Using the API key of the other mode: the Live and Test credentials are separate.
- A `url_callback` / `url_success` / `url_return` whose domain is not listed under the dashboard’s “Website domains”: the request is rejected.
- Treating `check` or `confirm_check` as paid. Only `paid` and `paid_over` mean paid; `wrong_amount` means less than the full amount arrived.
- Shipping twice because the same webhook arrived twice. Deduplicate by `uuid` + `status` (static wallets: by `uuid`).
- Hard-coding credentials. Use environment variables (`BW_MERCHANT`, `BW_API_KEY`) and keep them out of git.

## Acceptance checklist (run in test mode, report each result to the user)

Bills:
- [ ] Create a test bill with the Test credentials and get the payment page link (result.url)
- [ ] Open the payment page and click “Simulate payment” → full amount
- [ ] My server receives a webhook with status paid and the signature verifies
- [ ] The matching order is marked as paid
- [ ] The same webhook arriving again (resend it from the dashboard’s “Developers › Webhook log”, or POST /v1/payment/resend) does not ship twice

Static wallets:
- [ ] Open an address for a test user with the Test credentials and get the address and deposit page link
- [ ] Send a sample webhook with POST /v1/test-webhook/wallet
- [ ] My server receives a webhook with type wallet and the signature verifies
- [ ] That user’s balance goes up by payment_amount_usd
- [ ] The same webhook arriving again does not credit the balance twice

## Safety

- Use the **Test** credentials while developing. Never ask the user to paste the **Live** API key into the chat; they put it into the server environment themselves.
- Commit or back up before large edits so the user can roll back.
- Before going live, the user pays a small real amount (“Try a payment” on the dashboard home page) and checks the order and balance.
