# On-chain cards

Cards are held by Packflip by default. When a user wants the NFT, you can mint the card to their wallet on Base. A minted card can still be bought back, by burning the NFT.

On-chain actions are authorized by Packflip and sent from the **customer's wallet**. Packflip never holds your users' keys, and your API key never signs transactions.

| Environment | Chain |
| --- | --- |
| Production | Base (`8453`) |
| Sandbox | Base Sepolia (`84532`) |

## 1. Request a mint

Minting is free: nothing is charged to your balance. The customer pays gas. `walletAddress` is the wallet that receives the NFTs; the customer does not need to register it first.

```bash
curl -X POST "$PACKFLIP_BASE_URL/api/v2/operations" \
  -H "Authorization: Bearer $PACKFLIP_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: mint-user_123-0001" \
  -d '{
    "kind": "mint",
    "customerId": "cus_…",
    "cardIds": ["card_…"],
    "walletAddress": "0xabc…"
  }'
```

The operation returns with `status: "awaiting_chain"` and one entry per card in `onchainOrders`:

```json
{
  "id": "oco_…",
  "cardId": "card_…",
  "chainId": 84532,
  "vendingMachineAddress": "0x…",
  "collectionAddress": "0x…",
  "tokenId": "1234",
  "buyerAddress": "0xabc…",
  "priceBaseUnits": "0",
  "deadline": "2026-09-17T08:15:00.000Z",
  "calldata": "0x…",
  "transactionHash": null,
  "confirmedAt": null
}
```

The cards must be revealed and held by the customer. Addresses are returned in lowercase.

## 2. Send the transaction from the customer's wallet

Send `calldata` to `vendingMachineAddress` with no value, before `deadline` (15 minutes after the authorization is issued). With viem:

```ts
import { createWalletClient, custom } from "viem";
import { baseSepolia } from "viem/chains";

const wallet = createWalletClient({ chain: baseSepolia, transport: custom(window.ethereum) });
const [account] = await wallet.requestAddresses();

const hash = await wallet.sendTransaction({
  account,
  to: order.vendingMachineAddress,
  data: order.calldata,
});
```

The transaction must come from `buyerAddress`, on `chainId`.

## 3. Wait for confirmation

Packflip watches the chain and confirms the mint on its own. When every card in the operation is minted, the operation becomes `completed`, each card's `onchain.mintedAt` is set, and `operation.completed` is emitted.

To speed this up, report the hash as soon as you have it. This is optional:

```bash
curl -X POST "$PACKFLIP_BASE_URL/api/v2/operations/op_…/onchain-orders/oco_…/submit" \
  -H "Authorization: Bearer $PACKFLIP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "transactionHash": "0x…" }'
```

A mint is accepted only if the transaction succeeded and either matches the signed calldata or mints that token to the wallet. Otherwise the order gets a `failureReason` and `operation.onchain_failed` is emitted.

If you think an event was missed, `POST /api/v2/operations/{operationId}/refresh` checks the chain immediately.

## Expired authorizations

If `deadline` passes before the transaction lands, choose one:

- **Re-sign**: `POST /api/v2/operations/{operationId}/resign` issues fresh signatures for the same token IDs and cards. Returns `409` if nothing has expired.
- **Cancel**: `POST /api/v2/operations/{operationId}/cancel` cancels the mint. The cards stay held, and `operation.onchain_cancelled` is emitted. Returns `409` while any signature is still valid.

Both first check the chain, so a transaction that landed late is confirmed rather than cancelled.

## Buy back a minted card

```bash
curl -X POST "$PACKFLIP_BASE_URL/api/v2/operations" \
  -H "Authorization: Bearer $PACKFLIP_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: buyback-onchain-user_123-0001" \
  -d '{
    "kind": "buyback",
    "mode": "onchain",
    "customerId": "cus_…",
    "cardIds": ["card_…"]
  }'
```

The operation returns `awaiting_chain` with one entry per card in `onchainBurns`, and the cards become `buyback_pending`. From the wallet that holds the NFT:

1. Approve the handler once per collection: call `setApprovalForAll(handlerAddress, true)` on `collectionAddress`.
2. Send `calldata` to `handlerAddress` before `deadline`.

```ts
import { erc721Abi } from "viem";

await wallet.writeContract({
  account,
  address: burn.collectionAddress,
  abi: erc721Abi,
  functionName: "setApprovalForAll",
  args: [burn.handlerAddress, true],
});

await wallet.sendTransaction({ account, to: burn.handlerAddress, data: burn.calldata });
```

When the burn confirms, the card becomes `bought_back`, the buyback value quoted when the operation was created is credited to your cash balance, and `buyback.completed` is emitted once every burn in the operation is settled. You can report the hash early with `POST /api/v2/operations/{operationId}/onchain-burns/{onchainBurnId}/submit`.

Expired burn authorizations can be re-signed or cancelled the same way as mints. A cancelled burn leaves the card minted and `active`.

## Sandbox notes

The sandbox mints on Base Sepolia from a test collection. You need Base Sepolia ETH for gas, available from public faucets. If on-chain actions are not configured in an environment, these requests return `503 service_unavailable`.
