TRENDDocs
Guides

Trade through the API

Quote, build unsigned, sign on your side, submit, check. The API never holds a key.

What is live today

TREND runs on its own dev validator. The mainnet programs are not deployed: their ids are reserved and no launch exists there. Statement as of 2026-10-10.

Four steps, and the split between them is the point.

POST /v1/trades/quote          price it
POST /v1/trades/build          get UNSIGNED bytes
        you sign, with your own key, wherever your key lives
POST /v1/transactions/submit   land it
GET  /v1/transactions/{sig}    check it

The backend never holds a private key. build returns base64 and stops. It reads the quote from the server's own store by id and does not accept a quote object back, so nothing between the price you were shown and the bytes you sign can change the amounts.

1. Quote

{
  "launch": "<launch address>",
  "direction": "buy",
  "amountIn": "250000000",
  "mode": "exactInSol",
  "slippageBps": 300,
  "wallet": "<your public address>",
  "preview": false
}

launch must be an address here. Unlike the read routes, a ticker is not accepted.

What amountIn counts

DirectionmodeamountIn is
buyexactInSolLamports, and a spending ceiling
buyexactOutLaunch tokens you want out
buyexactInThe protocol's internal quote unit
sellexactInLaunch tokens you give up
either, on a launch past the curveanyMode is ignored: a buy is lamports, a sell is launch tokens

exactInSol is a ceiling, not a target. The server reads each pool's reserves and solves for the largest trade the budget covers, so the trade cannot cost more than the number you named. It is the mode for "buy 1 SOL of this".

The fields that change the answer

  • slippageBps is your tolerance, default 100. On a buy it sizes an input buffer and any excess stays in your wallet, so a loose setting costs dust. On a sell it sets the floor on what you receive, so a loose setting is a real loss. Do not use one number for both.
  • wallet, when given, changes the answer: the quote reports which components the wallet already holds and routes only the shortfall. Without it you get a price and no route.
  • preview: true prices without storing a quote. The quoteId it returns cannot be built. Use it for a number shown while a person is still deciding, and leave it out when you mean to build.

A quote expires after 30 seconds. Read the shape you got back in the three quote shapes.

2. Build

{ "quoteId": "<from the quote>", "wallet": "<the wallet that signs>" }

The answer holds transaction (base64 of an unsigned versioned transaction) and lastValidBlockHeight. The wallet in the request is the fee payer. Balances are read again here, so the route can differ from the quote's copy, and the build's route is the authoritative one.

A plan with several steps

Sometimes the answer carries a plan:

{
  "transaction": "<the trade>",
  "plan": [
    { "step": 1, "label": "Create your token accounts for this pair", "transaction": "<base64>", "kind": "setup" },
    { "step": 2, "label": "Buy", "transaction": "<base64>", "kind": "trade", "settles": true }
  ]
}

Rules that have each broken a client:

  1. Sign and land the steps in order. A buy acquires components and then settles, so its detached swaps come before the trade. A sell settles first, so its swaps come after. Running them out of order or at once fails on an account that does not exist yet or a balance that is not there.
  2. transaction is always the last entry of plan. A client that ignores plan signs the right transaction and then fails loudly rather than doing something subtly wrong.
  3. Tell the person how many signatures there will be before the first one. The quote's route.transactions says it.
  4. Exactly one step has settles: true. That is the step the quoteId belongs on.

3. Sign

Deserialize each transaction, sign it with the wallet you named, and keep the bytes. The snippet signs with a throwaway key and stops before submitting. It is a real file, run against dev after each deploy of this site.

snippets/trade.mjs
// Trade end to end up to the signature, with a throwaway key. Needs @solana/web3.js.
// It quotes for real (preview: false), asks for the unsigned transaction, signs it locally and stops:
// it never submits, so nothing is spent. Replace the throwaway key with your own to go further.
import { Keypair, VersionedTransaction } from '@solana/web3.js';

const API = process.env.TREND_API ?? 'https://api.dev.trend.fun';
const wallet = Keypair.generate();

async function call(path, init) {
  const res = await fetch(API + path, init);
  return { status: res.status, body: await res.json() };
}
const post = (path, body) =>
  call(path, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) });

const { body: list } = await call('/v1/launches/trending');
const launch = list.launches.find((l) => l.kind === 'pairs' && l.status === 'activeBonding') ?? list.launches[0];

// Quote (preview false), so the server stores it and a build can find it by id.
const quoted = await post('/v1/trades/quote', {
  launch: launch.address,
  direction: 'buy',
  amountIn: '250000000',
  mode: 'exactInSol',
  wallet: wallet.publicKey.toBase58(),
  preview: false,
});
if (quoted.status !== 200) throw new Error(`quote refused: ${quoted.status} ${quoted.body.error}`);
const { quote } = quoted.body;

// Build: unsigned bytes. The wallet in the request is the fee payer.
const built = await post('/v1/trades/build', { quoteId: quote.quoteId, wallet: wallet.publicKey.toBase58() });
if (built.status !== 200) {
  // A fresh key holds nothing, so a refusal here is expected. It is a sentence in `error`.
  console.log('build refused:', built.status, built.body.error);
  process.exit(0);
}

// Sign every step in order. `transaction` is always the last step.
const steps = built.body.plan ? built.body.plan.map((s) => s.transaction) : [built.body.transaction];
for (const b64 of steps) {
  const tx = VersionedTransaction.deserialize(Buffer.from(b64, 'base64'));
  tx.sign([wallet]);
  console.log('signed a transaction of', tx.serialize().length, 'bytes');
}

4. Submit and check

Send the base64 of the signed transaction to POST /v1/transactions/submit with the quoteId, or send it through your own RPC. Then poll GET /v1/transactions/{signature}. Pass lastValidBlockHeight from the build so the route can say expired when the chain has moved past it with no sign of your transaction.

A transaction the network rejected comes back 200 with status: "failed" and the reason in error, not a 5xx: the API call worked and the transaction did not. Check status, not the HTTP code.

Everything above is described field by field in the API reference.

On this page