> ## 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.

# Buying Gold

> Quote a buy, co-sign the returned transaction with partner + user wallets, submit, and wait for on-chain confirmation.

## Overview

A buy converts the user's USDC into `$GOLD` on Solana. The flow is:

1. **Quote** — `POST /v1/buy` → GRAIL returns a partially-signed transaction with a quote
2. **Co-sign** — add the partner wallet signature and the user wallet signature
3. **Submit** — `POST /v1/buy/:trade_id/submit` (or broadcast directly to any Solana RPC)
4. **Wait** — the indexer writes the `Trade` row after on-chain confirmation (\~10–15s on devnet)
5. **Fetch** — `GET /v1/trades/:trade_id` once the indexer has caught up

The quote endpoint is **stateless** — nothing is written to GRAIL's database at quote time. The authoritative `Trade` record appears only after the transaction confirms on-chain.

## Signers — the most important thing to get right

A buy transaction requires **three** signatures:

| Signer         | What they sign for                                              | Who produces the signature                  |
| -------------- | --------------------------------------------------------------- | ------------------------------------------- |
| GRAIL (server) | A memo instruction carrying the `trade_id` for the indexer      | Already applied before GRAIL returns the tx |
| Partner wallet | `whitelisted_wallet` authorization in the inti swap instruction | You add this on the partner's behalf        |
| User wallet    | USDC token authority (the account that's sending USDC)          | You add this on the user's behalf           |

If any of the three is missing, `/submit` returns `400 broadcast_failed` with `Missing signature for public key <pubkey>`.

## The partially-signed transaction

The quote response contains `partially_signed_transaction` — a base64 string. It's a Solana `Transaction` that already has GRAIL's signature attached. You:

1. Deserialize it
2. `partialSign(partnerKeypair)` and `partialSign(userKeypair)`
3. Serialize it back to base64

<Warning>
  The transaction bakes in a Solana `recentBlockhash` that expires in **\~60 seconds**. If you hold it too long, `/submit` returns `broadcast_failed: Blockhash not found`. Re-quote and try again.
</Warning>

## Step 1 — Get a quote

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

curl -s -X POST "$BASE/v1/buy" \
  -H "x-api-key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "grail_user_id": "gu_6b60956e-a8ee-4de2-8128-04c7fdf633c3",
    "usdc_amount": 100,
    "slippage_bps": 50
  }' \
  > /tmp/quote.json

cat /tmp/quote.json
```

Response:

```json theme={null}
{
  "trade_id": "trd_4e7a1b8f-9c32-4a91-b3e6-7f12a8d4c5e9",
  "side": "buy",
  "quote": {
    "usdc_amount": 100,
    "gold_amount": 0.0205,
    "price_per_troy_oz": 4872.84,
    "fee_bps": 50,
    "fee_usd": 0.50,
    "min_gold_out": 0.0204
  },
  "partially_signed_transaction": "AQAAAAABAAEC...<base64>..."
}
```

### Slippage

Two ways to set the slippage floor:

* **`slippage_bps`** (default `50` = 0.5%) — GRAIL computes `min_gold_out = quoted_gold * (10000 - slippage_bps) / 10000`
* **`min_gold_out`** — absolute override. If supplied, `slippage_bps` is ignored.

On devnet the Pyth gold price updates infrequently (every few minutes). With tight slippage on a stale quote, trades can revert with `SlippageExceeded` on-chain. If you see this, widen to 100–300 bps.

## Step 2 — Co-sign with partner + user

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

const PARTNER_SK = bs58.decode(PARTNER_SECRET_BASE58);
const USER_SK = bs58.decode(USER_SECRET_BASE58);

// Deserialize the partial-signed tx
const pstxB64 = quote.partially_signed_transaction;
const tx = Transaction.from(Buffer.from(pstxB64, "base64"));

// Co-sign with partner, then user
tx.partialSign(Keypair.fromSecretKey(PARTNER_SK));
tx.partialSign(Keypair.fromSecretKey(USER_SK));

// Serialize the fully-signed tx back to base64
const signedB64 = tx.serialize().toString("base64");
```

<Tip>
  In a real deployment, the user signs on the client side and the partner's server appends the partner signature before broadcast. You don't have to co-sign in a single process — only the final serialized transaction matters.
</Tip>

## Step 3 — Submit

Two options — both reach the same indexer.

### Option A: Submit via GRAIL

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

Response:

```json theme={null}
{
  "trade_id": "trd_4e7a1b8f-9c32-4a91-b3e6-7f12a8d4c5e9",
  "tx_hash": "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp...base58"
}
```

GRAIL runs Solana's pre-flight simulation. If simulation fails (wrong signer, slippage blown up by a stale price, insufficient balance, etc.), you get `400 broadcast_failed` with the reason in `message`.

### Option B: Broadcast directly

```typescript theme={null}
import { Connection } from "@solana/web3.js";

const connection = new Connection("https://api.devnet.solana.com", "confirmed");
const txHash = await connection.sendRawTransaction(Buffer.from(signedB64, "base64"));
```

The indexer picks up any confirmed transaction carrying the `trade_id` memo — it doesn't matter who broadcasts.

<Note>
  If you want a transaction to actually land and revert on-chain (for testing the failure path), broadcast directly with `skipPreflight: true`. GRAIL's `/submit` pre-flight would otherwise reject the tx before it lands.
</Note>

## Step 4 — Wait and fetch

The indexer writes the `Trade` row after on-chain confirmation, typically 10–15 seconds on devnet. Polling `GET /v1/trades/:trade_id` right after submit returns `404 trade_not_found` until the indexer catches up.

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

Response (successful):

```json theme={null}
{
  "trade_id": "trd_4e7a1b8f-9c32-4a91-b3e6-7f12a8d4c5e9",
  "side": "buy",
  "status": "confirmed",
  "usdc_amount": 100,
  "gold_amount": 0.020528,
  "price_per_troy_oz": 4872.84,
  "fee_bps": 50,
  "fee_usd": 0.50,
  "submitted_tx_hash": "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp...base58",
  "created_at": "2026-04-17T10:18:30.000Z",
  "updated_at": "2026-04-17T10:18:30.000Z"
}
```

A `status: "failed"` row is written if the transaction landed on-chain but the inti program reverted (e.g., `SlippageExceeded`). On failures, `usdc_amount` reflects the input from the memo; `gold_amount` and the price/fee fields are `0` (the program never computed them).

## End-to-end reference 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/buy" \
  -H "x-api-key: $API_KEY" -H "Content-Type: application/json" \
  -d "{\"grail_user_id\":\"$USER_ID\",\"usdc_amount\":10,\"slippage_bps\":50}" \
  > /tmp/quote.json

TRADE_ID=$(jq -r .trade_id /tmp/quote.json)
PSTX=$(jq -r .partially_signed_transaction /tmp/quote.json)

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

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

# 4. Wait + fetch
sleep 15
curl -s "$BASE/v1/trades/$TRADE_ID" -H "x-api-key: $API_KEY"
```

## Common errors

| Error                                                        | Likely cause                                                                |
| ------------------------------------------------------------ | --------------------------------------------------------------------------- |
| `400 kyc_level_insufficient`                                 | User's KYC is not `full`                                                    |
| `400 onchain_config_missing`                                 | ORO hasn't set up your partner's on-chain config yet. Contact ORO.          |
| `400 broadcast_failed: Blockhash not found`                  | Co-signed too slowly — quote's blockhash expired. Re-quote.                 |
| `400 broadcast_failed: Missing signature for public key ...` | Didn't co-sign with all three required signers                              |
| `400 broadcast_failed: SlippageExceeded`                     | Price moved between quote and submit beyond `min_gold_out`. Widen slippage. |
| `404 trade_not_found` (right after submit)                   | Indexer hasn't written the row yet. Wait 15s and retry.                     |
| `503 pricing_unavailable`                                    | Pyth oracle unreachable or returned data older than the staleness window    |

## Next steps

Close the loop with [Selling Gold](/guides/selling-gold), or jump to [Redeeming Physical Gold](/guides/redeeming-physical-gold) for physical fulfillment.
