# Building with a coding agent

These docs are written to be read by coding agents such as Claude Code, Cursor, or Codex, as well as by people.

## Machine-readable resources

| Resource | URL | Use it for |
| --- | --- | --- |
| `llms.txt` | [/llms.txt](/llms.txt) | An index of every guide, with links. |
| `llms-full.txt` | [/llms-full.txt](/llms-full.txt) | All guides in one Markdown file, for a single fetch. |
| OpenAPI 3.1 | [/openapi.json](/openapi.json) | Exact request and response schemas. Generate types or a client from it. |
| Guides as Markdown | `/docs/{guide}.md`, for example [/docs/quickstart.md](/docs/quickstart.md) | Fetching one guide without HTML. |

Both hosts serve the same documents; the prompt below reads them from the sandbox. If your agent's browser cannot open `text/markdown`, use `llms-full.txt` or the HTML pages at `/docs/{guide}`.

## A prompt to start with

Paste this into your agent, and adjust the first paragraph to your product:

```text
Integrate the Packflip Partner API into this project so users can buy card packs,
see their cards, and sell cards back.

Read https://sandbox.partner.packflip.xyz/llms-full.txt first, and use
https://sandbox.partner.packflip.xyz/openapi.json as the source of truth for
request and response shapes. Follow the "Off-chain lifecycle" guide in it for
the purchase flow.

Requirements:
- Call the API only from server code. Read PACKFLIP_BASE_URL, PACKFLIP_API_KEY,
  and PACKFLIP_WEBHOOK_SECRET from the environment. Default the base URL to
  https://sandbox.partner.packflip.xyz.
- Create one Packflip customer per user, with externalUserId set to our user ID,
  and store the returned cus_ ID. On 409, look the customer up by external ID.
- Send an Idempotency-Key on every POST /api/v2/operations. Derive it from a
  database record created before the call, and reuse it on retries.
- Treat money as decimal strings. Never convert to floating point.
- Add a webhook route that verifies webhook-signature against the raw body,
  deduplicates on the event id, and refreshes state from the API.
- Branch on error.code, not on the HTTP status: 409 can be
  insufficient_balance, out_of_stock, conflict, or idempotency_conflict.
- Offer off-chain buyback and redemption only for cards whose status is active,
  sealed is false, and onchain is null. Re-fetch cards after every operation.
- Write tests against the sandbox or with recorded responses; do not call
  production from tests.
```

## Rules an integration must follow

1. **Keys stay on the server.** An API key controls your whole balance.
2. **One environment per configuration.** A base URL, key, and webhook secret always belong together. Sandbox is `https://sandbox.partner.packflip.xyz`; production is `https://partner.packflip.xyz`.
3. **Idempotency on every operation.** Retries must reuse the key; new purchases must use a new one.
4. **Decimal money.** Amounts are strings such as `"1.500000"`.
5. **Strict requests, tolerant responses.** Unknown request fields are rejected; unknown response fields must be ignored.
6. **Webhooks are hints.** Verify, deduplicate, and re-read the resource.
7. **On-chain transactions come from the user's wallet.** The API returns calldata; it never takes custody of user keys.

## A minimal client

```ts
// packflip.ts: server-only
const baseUrl = process.env.PACKFLIP_BASE_URL ?? "https://sandbox.partner.packflip.xyz";
const apiKey = process.env.PACKFLIP_API_KEY!;

export class PackflipError extends Error {
  constructor(readonly status: number, readonly code: string, message: string) {
    super(message);
  }
}

export async function packflip<T>(
  path: string,
  init: { method?: string; body?: unknown; idempotencyKey?: string } = {},
): Promise<T> {
  const response = await fetch(`${baseUrl}/api/v2${path}`, {
    method: init.method ?? (init.body ? "POST" : "GET"),
    headers: {
      Authorization: `Bearer ${apiKey}`,
      ...(init.body ? { "Content-Type": "application/json" } : {}),
      ...(init.idempotencyKey ? { "Idempotency-Key": init.idempotencyKey } : {}),
    },
    body: init.body ? JSON.stringify(init.body) : undefined,
    cache: "no-store",
  });
  if (response.status === 204) return undefined as T;
  const json = await response.json();
  if (!response.ok) throw new PackflipError(response.status, json.error?.code, json.error?.message);
  return json as T;
}

// Example: draw one pack for a customer.
export function drawPack(customerId: string, vendingMachineId: number, orderId: string) {
  return packflip<{ id: string; status: string; cards: unknown[]; replayed: boolean }>("/operations", {
    body: { kind: "order", customerId, vendingMachineId, quantity: 1 },
    idempotencyKey: `order-${orderId}`,
  });
}
```

For typed responses, generate types from the OpenAPI document, for example with `npx openapi-typescript https://sandbox.partner.packflip.xyz/openapi.json -o packflip.d.ts`. For a complete purchase flow with retries, see [Off-chain lifecycle](/docs/lifecycle).

## Guides

- [Sandbox quickstart](/docs/quickstart)
- [Off-chain lifecycle](/docs/lifecycle)
- [Core concepts](/docs/concepts)
- [Funding your balance](/docs/funding)
- [Cards and operations](/docs/cards)
- [On-chain cards](/docs/onchain)
- [Webhooks](/docs/webhooks)
- [Errors, idempotency, and retries](/docs/errors)
- [Going live](/docs/going-live)
