> ## Documentation Index
> Fetch the complete documentation index at: https://docs.grail.oro.finance/llms.txt
> Use this file to discover all available pages before exploring further.

# Redeeming Physical Gold

> Convert a user's $GOLD tokens into a physical-gold denomination for pickup. Two-signer transaction (GRAIL + user), and a multi-step admin lifecycle for fulfillment.

## Overview

A redemption converts a user's `$GOLD` tokens into a physical-gold denomination (e.g., `1 Tola`) that ORO fulfills off-chain in a specific city. The token flow is on-chain — the user transfers tokens into GRAIL's escrow. Fulfillment (physical pickup) is managed by ORO and progresses through a status lifecycle that the partner can observe but not drive.

Redemptions differ from trades in three important ways:

1. **Only two signers** — GRAIL + user. **The partner does not sign.**
2. **Row written at quote time** — a redemption row is created at status `quoted` as soon as you call `POST /v1/redemptions` (unlike trades, which are stateless at quote time).
3. **`quoted` is never surfaced** — `GET /v1/redemptions/:id` returns `404` until the user's transfer has been confirmed on-chain and the indexer advances status to `submitted`.

## Status lifecycle

```
quoted (internal — never visible via API)
  └─► submitted              ← indexer writes after on-chain confirmation
        ├─► preparing        ← ORO admin: starting physical pickup prep
        │     └─► ready      ← ORO admin: gold ready at pickup location
        │           └─► collected  (FINAL)
        │
        └─► cancellation_requested  ← partner (only valid at `submitted`)
              └─► cancelled        (FINAL)
```

* Indexer writes `quoted → submitted` (or `quoted → failed` if the on-chain transfer reverts)
* ORO admin drives `submitted → preparing → ready → collected`
* Partner can request cancel **only while status is `submitted`** — once ORO advances to `preparing`, physical prep has begun and cancellation is a coordination question, not an automated one

## Step 1 — Look up available denominations

```bash theme={null}
BASE=https://grail-stack-dev.onrender.com

curl -s "$BASE/v1/denominations?country=PK" \
  -H "x-api-key: $API_KEY"
```

Response:

```json theme={null}
{
  "denominations": [
    {
      "id": "pk_tola_1",
      "label": "1 Tola",
      "weight_g": 11.664,
      "weight_troy_oz": 0.375,
      "city": "karachi"
    }
  ]
}
```

Pick the `id` for [Step 2](#step-2-quote-a-redemption) and note the `city` — you must pass the same city in the quote body (case-insensitive).

## Step 2 — Quote a redemption

```bash theme={null}
curl -s -X POST "$BASE/v1/redemptions" \
  -H "x-api-key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "grail_user_id": "gu_6b60956e-a8ee-4de2-8128-04c7fdf633c3",
    "denomination_id": "pk_tola_1",
    "city": "karachi"
  }'
```

Response:

```json theme={null}
{
  "redemption_id": "red_a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
  "quote": {
    "denomination": "1 Tola",
    "weight_g": 11.664,
    "spot_price_usd": 4872.84,
    "gold_value_usd": 1827.34,
    "fee_usd": 0,
    "total_usd": 1827.34,
    "tokens_required": "0.375006"
  },
  "partially_signed_transaction": "AQAAAAABAAEC...<base64>..."
}
```

<Warning>
  `tokens_required` is the amount of **`$GOLD` tokens** (per troy ounce, 6 decimals) the user must transfer. In the example above: `0.375006` tokens for 1 Tola. This is **not** the same as `total_usd` (`1827.34`) — that's the USD value of those tokens. Mixing them up is the most common redemption mistake.
</Warning>

## Step 3 — Co-sign with the user wallet ONLY

Unlike trades, the partner does not sign. Only the user needs to add their signature.

```typescript theme={null}
import { Transaction, Keypair } from "@solana/web3.js";
import bs58 from "bs58";

const tx = Transaction.from(Buffer.from(quote.partially_signed_transaction, "base64"));
tx.partialSign(Keypair.fromSecretKey(bs58.decode(USER_SECRET_BASE58)));
const signedB64 = tx.serialize().toString("base64");
```

## Step 4 — Submit

```bash theme={null}
curl -s -X POST "$BASE/v1/redemptions/$REDEMPTION_ID/submit" \
  -H "x-api-key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"signed_tx\":\"$SIGNED_B64\"}"
```

Response:

```json theme={null}
{
  "redemption_id": "red_a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
  "tx_hash": "4ABC...base58"
}
```

Same passthrough semantics as trade submit — no DB write. The indexer writes the status transition after on-chain confirmation.

## Step 5 — Wait, then fetch

```bash theme={null}
sleep 15
curl -s "$BASE/v1/redemptions/$REDEMPTION_ID" -H "x-api-key: $API_KEY"
```

Once the indexer has advanced the row:

```json theme={null}
{
  "redemption_id": "red_a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
  "status": "submitted",
  "denomination": "1 Tola",
  "weight_g": 11.664,
  "city": "karachi",
  "quote": {
    "spot_price_usd": 4872.84,
    "gold_value_usd": 1827.34,
    "fee_usd": 0,
    "total_usd": 1827.34,
    "tokens_required": "0.375006"
  },
  "submitted_tx_hash": "4ABC...base58",
  "cancellation_reason": null,
  "created_at": "2026-04-17T11:00:00.000Z",
  "updated_at": "2026-04-17T11:00:15.000Z"
}
```

While the redemption is still at `quoted`, this endpoint returns `404 redemption_not_found` — treat that as "indexer hasn't confirmed yet", not as an error.

## Tracking fulfillment

The partner cannot drive lifecycle transitions from `submitted` onward. ORO does that in the admin interface. As a partner, poll `GET /v1/redemptions/:id` (or use [List Redemptions](/api-reference/redemptions/list-redemptions) with a status filter) to see when the row advances. Typical transitions:

* `submitted` → `preparing` when ORO begins physical handling
* `preparing` → `ready` when gold is at the pickup location
* `ready` → `collected` after the user collects

## Cancelling

Only valid at `submitted`:

```bash theme={null}
curl -s -X POST "$BASE/v1/redemptions/$REDEMPTION_ID/cancel" \
  -H "x-api-key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"reason":"user changed mind"}'
```

Response:

```json theme={null}
{
  "redemption_id": "red_a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
  "status": "cancellation_requested"
}
```

If the redemption has moved past `submitted`, you get `400 cancellation_not_allowed`. From `cancellation_requested`, ORO advances to `cancelled` (final) after confirming no physical prep has started.

<Warning>
  The user's `$GOLD` has already been transferred to escrow at `submitted`. Cancellation flags the record — it does NOT automatically return the tokens to the user. Any refund is a coordination question with ORO, not an automated flow.
</Warning>

## Listing

```bash theme={null}
curl -s "$BASE/v1/redemptions?status=submitted" -H "x-api-key: $API_KEY"
```

Optional filters: `grail_user_id`, `status`, `city`. `quoted` is always excluded.

## Common errors

| Error                                       | Likely cause                                                                             |
| ------------------------------------------- | ---------------------------------------------------------------------------------------- |
| `400 invalid_denomination`                  | `denomination_id` doesn't exist or is inactive                                           |
| `400 invalid_city`                          | `city` in the request body doesn't match the denomination's registered city              |
| `400 kyc_level_insufficient`                | User's KYC is not `full`                                                                 |
| `400 broadcast_failed: Blockhash not found` | Co-signed too slowly — blockhash expired. Re-quote.                                      |
| `400 broadcast_failed: insufficient funds`  | User's `$GOLD` balance was insufficient for `tokens_required`                            |
| `400 cancellation_not_allowed`              | Redemption has already moved past `submitted`                                            |
| `404 redemption_not_found`                  | Redemption is still at internal `quoted` (indexer hasn't confirmed yet) or doesn't exist |

## End-to-end script

```bash theme={null}
#!/usr/bin/env bash
set -euo pipefail

BASE=https://grail-stack-dev.onrender.com
USER_ID=gu_6b60956e-a8ee-4de2-8128-04c7fdf633c3

# 1. Quote
curl -s -X POST "$BASE/v1/redemptions" \
  -H "x-api-key: $API_KEY" -H "Content-Type: application/json" \
  -d "{\"grail_user_id\":\"$USER_ID\",\"denomination_id\":\"pk_tola_1\",\"city\":\"karachi\"}" \
  > /tmp/rq.json

RID=$(jq -r .redemption_id /tmp/rq.json)
PSTX=$(jq -r .partially_signed_transaction /tmp/rq.json)

# 2. Co-sign with USER wallet only (no partner signature on redemptions)
SIGNED=$(npx tsx -e "
import { Transaction, Keypair } from '@solana/web3.js';
import bs58 from 'bs58';
const user = Keypair.fromSecretKey(bs58.decode('$USER_SK'));
const tx = Transaction.from(Buffer.from(process.argv[1], 'base64'));
tx.partialSign(user);
process.stdout.write(tx.serialize().toString('base64'));
" "$PSTX")

# 3. Submit
curl -s -X POST "$BASE/v1/redemptions/$RID/submit" \
  -H "x-api-key: $API_KEY" -H "Content-Type: application/json" \
  -d "{\"signed_tx\":\"$SIGNED\"}"

# 4. Poll until status=submitted
sleep 15
curl -s "$BASE/v1/redemptions/$RID" -H "x-api-key: $API_KEY"
```
