# Task: integrate Amboss Payments (TypeScript SDK)

You are integrating Amboss Payments into this TypeScript codebase using the
official SDK, `@ambosstech/payments`. Amboss Payments settles USDT, USDC, and
BTC over the Lightning Network with sub-second finality and no chargebacks.

Docs: https://docs.amboss.tech/sdk

Work in the sandbox environment until the whole flow passes end to end. Do not
touch live keys or live wallets unless I explicitly ask you to.

## Rules you must not break

1. **Amounts are strings in the asset's minor units.** `"1000"` sats for BTC,
   `"1000000"` for 1 USDT. Never a number, never a float.
2. **Never hard-code an `asset_id` or a precision.** Read it from
   `wallet.asset.precision` at runtime.
3. **In sandbox, `metadata.amb_sandbox_behavior` decides the outcome and
   defaults to `expire`.** Set `"complete"`, `"fail"`, or `"expire"`
   deliberately, or your test invoice just times out.
4. **Verify every webhook over the raw request body**, before acting on it.
5. **Dedupe on `event.id`.** Delivery is at-least-once.
6. **Pass `idempotencyKey` on every send.** A retry without one can pay twice.
7. **Secrets stay in environment variables**, never in source, logs, or
   fixtures. The team `password` in particular is used to decrypt the node
   macaroon *in your own process* and is never sent to the API — keep it that
   way.

## Install

```bash
pnpm add @ambosstech/payments
```

Use whichever package manager this repo already uses. `@ambosstech/core`
arrives transitively; nothing else to install. Requires Node.js 18.18+.

## Environment variables to add

Add these to the repo's env schema / `.env.example` using its existing
conventions. Never commit real values.

- `AMBOSS_API_KEY` — service API key (`amb_test_…` sandbox, `amb_live_…` live)
- `AMBOSS_WALLET_ID` — the wallet the integration receives into
- `AMBOSS_WEBHOOK_SECRET` — the endpoint secret used to verify deliveries

## Build this

### 1. One shared client

Construct the client once and export it from a single module. Do not
instantiate it per request.

```ts
import { Payments } from "@ambosstech/payments";

export const payments = new Payments({
  serviceApiKey: process.env.AMBOSS_API_KEY,
  webhookSecret: process.env.AMBOSS_WEBHOOK_SECRET,
  timeoutMs: 30_000, // default
});
```

Both fields are optional: a service that only receives webhooks needs just
`webhookSecret`. Touching `payments.environments`, `payments.wallets`, or
`payments.transactions` without a `serviceApiKey` throws `ConfigError`.
`baseUrl` and `fetch` are overridable — inject `fetch` to stub the network
in tests rather than mocking modules.

If this repo has a DI container (NestJS, tsyringe, etc.), register the client
there and follow the existing provider conventions instead of exporting a
module-level singleton.

### 2. Wallet discovery

```ts
const environments = await payments.environments.list();
const wallets = await payments.wallets.list({ environmentId });
const wallet = await payments.wallets.get(walletId);

wallet.asset.precision; // use this for every amount conversion
wallet.balance;
```

`wallets.list` returns a trimmed record for fast listing. Call
`wallets.get(id)` when you need `balance`, `asset`, or attached `nodes`.

### 3. Receiving

```ts
const transaction = await payments.transactions.createReceive({
  wallet_id: walletId,
  amount: "1000",
  description: "Order #1234",
  expires_in_seconds: 3600,
  idempotency_key: `order-${orderId}-attempt-${attempt}`,
  metadata: { order_id: String(orderId) },
});

transaction.payment_request; // BOLT11 to render as a QR or lightning: link
transaction.payment_hash;
```

Note the shape difference from `send`: `createReceive` takes snake_case API
fields; `send` takes camelCase. Metadata here is a real object — the SDK
serializes it for you. It round-trips onto the webhook, so put your order id in
it and correlate on that.

### 4. Sending

```ts
const { transaction, payment } = await payments.transactions.send({
  walletId,
  password,                                  // live only; decrypts the macaroon locally
  destination: { bolt11: "lnbc1..." },       // or { lightningAddress, amountSats }
  idempotencyKey: `payout-${payoutId}`,
  onUpdate: ({ status }) => log(status),     // INITIATED | IN_FLIGHT | SUCCEEDED | FAILED
  signal: abortController.signal,
});

payment?.status;      // 'SUCCEEDED' | 'FAILED' — null in sandbox
payment?.feeSat;
```

On live wallets `send` decrypts the node's admin macaroon in-process with the
team `password` and drives the payment directly against the node, resolving
with the terminal result. The SDK picks LND for base-asset wallets and litd for
Taproot Asset wallets automatically — do not wire that yourself.

**In sandbox there is no password and `payment` comes back `null`.** The
backend settles asynchronously per `metadata.amb_sandbox_behavior`. Observe
the outcome through webhooks; do not assume a null `payment` means failure.

### 5. The webhook handler

```ts
const event = payments.webhooks.verify({
  payload: rawBody,                      // string | Buffer — the RAW body
  signature: headers["x-webhook-signature"],
  timestamp: headers["x-webhook-timestamp"],
  toleranceSeconds: 300,                 // default
});

event.id;                                // dedupe on this
event.event_type;                        // payment.pending | completed | failed | expired
event.data.direction;                    // 'send' | 'receive'
event.data.status;
event.data.settle_amount;                // credit this, not data.amount — the fee sits in between
event.data.metadata;                     // your round-tripped order_id
```

There is also a static form, `Payments.webhooks.verify({ secret, payload,
signature, timestamp })`, for stateless handlers with no client.

Capture the raw body **before any JSON parser runs** — Express:
`express.raw({ type: "application/json" })`; NestJS:
`NestFactory.create(AppModule, { rawBody: true })` then `req.rawBody`; Fetch:
`await request.text()`. A re-serialized body will not match the HMAC.

Respond `200` quickly and queue the real work. Failures are retried at 10s,
60s, then 10 minutes, up to 8 attempts.

### 6. Error handling

Branch on the typed classes, never on message strings:

```ts
import {
  ApiError,               // API returned an error; carries status + graphqlErrors
  ConfigError,            // SDK misconfigured, e.g. missing serviceApiKey
  NetworkError,           // never reached the API
  WebhookVerificationError, // signature failed; carries a typed .code
  DecryptionError,        // wrong team password
  PaymentSendError,       // the node rejected or failed the payment
} from "@ambosstech/payments";
```

`WebhookVerificationError.code` is one of `missing_secret`,
`missing_signature`, `missing_timestamp`, `invalid_timestamp`,
`timestamp_out_of_tolerance`, `invalid_signature_format`,
`signature_mismatch`, `invalid_payload_json`. Map it to a 401 and log the
code, never the payload.

## Before you tell me you are done

- [ ] No API key, webhook secret, or team password in source, logs, tests, or
      fixtures.
- [ ] Every amount is a string in minor units derived from
      `wallet.asset.precision`.
- [ ] The webhook handler reads the raw body, calls `webhooks.verify`, and
      dedupes on `event.id` before any side effect.
- [ ] Every send passes an `idempotencyKey`.
- [ ] Errors branch on the typed classes above, not on strings.
- [ ] A sandbox receive with `amb_sandbox_behavior: "complete"` settles and
      the handler credits the order exactly once, including on a replayed
      delivery.
- [ ] The repo's existing lint, typecheck, and test commands pass. Show me the
      output.

Follow this repo's existing conventions for structure, naming, error handling,
and tests. Do not add dependencies the codebase does not already have.

## Reference

- SDK overview — https://docs.amboss.tech/sdk
- Configuration — https://docs.amboss.tech/sdk/configuration
- Environments and wallets — https://docs.amboss.tech/sdk/environments-and-wallets
- Transactions — https://docs.amboss.tech/sdk/transactions
- Webhooks — https://docs.amboss.tech/sdk/webhooks
- Errors — https://docs.amboss.tech/sdk/errors
- Underlying GraphQL API — https://docs.amboss.tech/payments
- Machine-readable index of all docs — https://docs.amboss.tech/llms.txt
