Skip to main content

Overview

A buy converts the user’s USDC into $GOLD on Solana. The flow is:
  1. QuotePOST /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. SubmitPOST /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. FetchGET /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: 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
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.

Step 1 — Get a quote

Response:

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

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.

Step 3 — Submit

Two options — both reach the same indexer.

Option A: Submit via GRAIL

Response:
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

The indexer picks up any confirmed transaction carrying the trade_id memo — it doesn’t matter who broadcasts.
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.

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.
Response (successful):
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

Common errors

Next steps

Close the loop with Selling Gold, or jump to Redeeming Physical Gold for physical fulfillment.