Skip to main content

Overview

A sell converts the user’s $GOLD tokens back into USDC on Solana. The flow and signer set are identical to Buying Gold — the only differences are the endpoint (/v1/sell) and the input/output fields (you specify gold_amount instead of usdc_amount; min_usdc_out replaces min_gold_out). Because everything else is the same — three signers, blockhash TTL, stateless quote, indexer-driven row writes — this guide focuses on the differences.

Signers

Same as buy: GRAIL + partner + user. Three signatures required.

Step 1 — Quote

BASE=https://grail-stack-dev.onrender.com

curl -s -X POST "$BASE/v1/sell" \
  -H "x-api-key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "grail_user_id": "gu_6b60956e-a8ee-4de2-8128-04c7fdf633c3",
    "gold_amount": 0.01,
    "slippage_bps": 50
  }'
Response:
{
  "trade_id": "trd_8b21d7f3-2a4c-4b5e-9d1f-3e8a7c6b5d4e",
  "side": "sell",
  "quote": {
    "gold_amount": 0.01,
    "usdc_amount": 48.49,
    "price_per_troy_oz": 4872.84,
    "fee_bps": 75,
    "fee_usd": 0.37,
    "min_usdc_out": 48.25
  },
  "partially_signed_transaction": "AQAAAAABAAEC...<base64>..."
}

Sell-specific input fields

FieldRequiredNotes
gold_amountyes$GOLD tokens to sell (human decimal).
slippage_bpsnoDefault 50 (0.5%). Ignored if min_usdc_out is set.
min_usdc_outnoAbsolute USDC floor. Overrides slippage_bps when provided.

Sell-specific fee note

The quote’s fee_bps often differs from a buy quote — on-chain inti uses separate market_open_fee_bps and market_close_fee_bps values, with the applicable side depending on market state. A partner’s IntegratorV2 on-chain config can also have a market_fee_override that forces one or the other. You don’t need to manage this — GRAIL reads the effective rate from the partner’s on-chain config when it builds the quote.

Step 2 — Co-sign with partner + user

Identical to buy:
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(PARTNER_SECRET_BASE58)));
tx.partialSign(Keypair.fromSecretKey(bs58.decode(USER_SECRET_BASE58)));
const signedB64 = tx.serialize().toString("base64");
The only difference from a buy tx is the on-chain program’s internal direction — GOLD out of the user’s token account, USDC in. You don’t touch that; it’s baked into the partial-signed transaction.

Step 3 — Submit

curl -s -X POST "$BASE/v1/sell/$TRADE_ID/submit" \
  -H "x-api-key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"signed_tx\":\"$SIGNED_B64\"}"
Response:
{
  "trade_id": "trd_8b21d7f3-2a4c-4b5e-9d1f-3e8a7c6b5d4e",
  "tx_hash": "3xJk9Pq2n...base58"
}
Pure passthrough — no DB write. As with buys, you can also broadcast directly to any Solana RPC.

Step 4 — Wait and fetch

sleep 15
curl -s "$BASE/v1/trades/$TRADE_ID" -H "x-api-key: $API_KEY"
The response shape is identical to Get Trade, with side: "sell". usdc_amount is the output, gold_amount is the input.

End-to-end script

Same as the buy reference script in Buying Gold, with two changes:
  1. Quote endpoint → /v1/sell
  2. Quote body → {"grail_user_id":"...", "gold_amount":0.01, "slippage_bps":50}
Everything else — co-sign, submit endpoint (/v1/sell/$TRADE_ID/submit), indexer polling — is identical.

Next steps

Flow complete? Move on to Redeeming Physical Gold for physical fulfillment.