Prompt for Agents

A copy-paste prompt that gives Claude Code, Codex, Cursor, or any coding agent everything it needs to integrate the @ambosstech/payments TypeScript SDK correctly.

Working with a coding agent? Paste the prompt below into Claude Code, Cursor, Codex, Copilot, or whatever you use. It carries the client setup, every method signature your integration needs, the typed error classes, and the handful of rules agents reliably get wrong — string amounts in minor units, the sandbox expire default, raw-body webhook verification, and idempotency on sends.

Integrate the TypeScript SDK

Prompt

Wires up @ambosstech/payments end to end: client, wallet discovery, receive, send, verified webhooks, and typed errors.

Open raw
# 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
amboss-payments-sdk.md · 212 lines

Not on TypeScript? Use the Payments API prompt instead. It targets the raw GraphQL endpoint, which is callable from any language.

How to use it

Paste it into your agent

Copy the prompt and drop it into your agent's session, in the repo where the integration should land. It's written to be self-contained — no other page needs to be open.

Add your own context

Tell it what you're building ("add a payout worker that pays Lightning Addresses") and point it at the module that should own the integration. The prompt tells the agent to reuse your existing client, DI container, and error conventions rather than inventing new ones.

Keep it in sandbox

The prompt already instructs the agent to stay in sandbox and to set amb_sandbox_behavior deliberately. It also warns that sandbox sends resolve with a null payment — a detail agents otherwise read as a failure.

Walk the self-check

The prompt closes with a checklist — no leaked secrets, string amounts, verified webhooks, idempotent sends, typed error branching. Ask the agent to walk it item by item and show you the lint, typecheck, and test output.

Feeding docs to your agent

Everything on this site is also available as plain text, for agents that fetch rather than browse:

ResourceWhat it is
/prompts/payments-sdk.mdThis prompt, as raw Markdown
/prompts/payments-api.mdThe GraphQL API prompt, as raw Markdown
/llms.txtA machine-readable index of every page on this site

Point an agent at /llms.txt when it needs to look something up mid-task, and at a prompt when it needs to know how to build.

Review what your agent writes before you ship it. The prompt tells it to keep the API key, webhook secret, and team password in environment variables and out of source, but you own the final diff — check for leaked secrets, and confirm the webhook handler passes the raw body to webhooks.verify.

What the prompt covers

AreaWhat the agent is told
ClientConstruct Payments once and share it; serviceApiKey and webhookSecret are independently optional
AmountsStrings in minor units, read from wallet.asset.precision — never a number
Receivingtransactions.createReceive takes snake_case fields and an object metadata that round-trips onto the webhook
Sendingtransactions.send takes camelCase, decrypts the macaroon in-process on live, and returns a null payment in sandbox
WebhooksInstance and static verify, raw-body capture per framework, event.id dedupe, settle_amount over amount
ErrorsBranch on ApiError, ConfigError, NetworkError, WebhookVerificationError, DecryptionError, PaymentSendError — never on message strings