Welcome to API Academy

Module 06 · API basics · ~25 min

Positions.

By the end of this module, your bot will know its own score, how much it’s holding right now, how much it has won or lost, and whether what the API says it owns matches what the chain says it owns.

To get there, you’ll pull your live portfolio, replay every fill, and compute realised + unrealised PnL from first principles. The same data powers your dashboards, your risk checks, and the on-chain reconciliation every production trading bot needs.

API basics tier · Reference card
Quick answer

How do you read your positions from the Limitless API?

One call: GET /portfolio/positions, wrapped by the SDK’s PortfolioFetcher.getPositions(), returns everything you hold, CLOB, AMM, and group positions plus accumulated points and current rewards, using just your API key, no wallet signing. Fills come from two endpoints: GET /portfolio/trades returns a flat array of AMM trades in a single call, while GET /portfolio/history is the cursor-paginated feed (cursor + limit) that also covers CLOB fills, splits, merges, and claims. From those you compute both PnL numbers: realised is (exit price minus entry price) times closed size, minus fees, replayable from fills; unrealised is open size times (current mark minus average entry), moving with the book. Authenticated calls carry the three HMAC headers (lmts-api-key, lmts-timestamp, lmts-signature); the legacy X-API-Key is deprecated. The REST view is an indexed cache, so reconcile against on-chain balanceOf on a schedule.

Endpoints verified 2026-06-10 against the OpenAPI spec.

Authenticated calls carry the three HMAC headers (lmts-api-key + lmts-timestamp + lmts-signature) from Module 03; the legacy X-API-Key is deprecated.

Section 01

Reading your portfolio.

GET /portfolio/positions returns everything you hold in one call: CLOB positions, AMM positions, group positions, accumulated points, and current rewards. The SDK’s PortfolioFetcher.getPositions() wraps it.

How to run this

  1. Keep LIMITLESS_API_KEY set, this is a read-only call, no wallet signing needed.
  2. Save as portfolio-positions.ts, then run npx tsx portfolio-positions.ts.
  3. You see your points total, reward balance, CLOB row count, and one line per CLOB position with YES and NO cost vs market value. Empty output is fine, you just haven’t opened positions yet.
// Module 06, Reading Your Portfolio
// Pull CLOB + AMM positions with rewards and accumulated points.

import { HttpClient, PortfolioFetcher } from '@limitless-exchange/sdk';

const httpClient = new HttpClient({
  baseURL: 'https://api.limitless.exchange',
  apiKey:  process.env.LIMITLESS_API_KEY,
});

async function readPortfolio() {
  const portfolio = new PortfolioFetcher(httpClient);
  const positions = await portfolio.getPositions();

  console.log('points total:', positions.accumulativePoints);
  console.log('rewards    :', positions.rewards);
  console.log('CLOB rows  :', positions.clob.length);
  console.log('AMM rows   :', positions.amm.length);

  for (const p of positions.clob) {
    const { yes, no } = p.positions;
    console.log(
      p.market.slug.padEnd(40),
      `YES cost=${yes.cost} mktVal=${yes.marketValue}`,
      `NO cost=${no.cost} mktVal=${no.marketValue}`,
    );
  }
}

readPortfolio().catch(console.error);

Section 02

Fetching fills.

Positions tell you where you are. Fills tell you how you got there. GET /portfolio/trades returns every execution; each row has the market, side, size, price, fees, and timestamp.

// Module 06, Fetching Fills
// GET /portfolio/trades, raw HTTP (no SDK method exists for this endpoint).

import { createHmac } from 'node:crypto';

const BASE = 'https://api.limitless.exchange';

function signedHeaders(method: string, pathWithQuery: string, body = ''): Record<string, string> {
  const ts  = new Date().toISOString();
  const msg = `${ts}\n${method}\n${pathWithQuery}\n${body}`;
  const sig = createHmac('sha256', Buffer.from(process.env.LMTS_TOKEN_SECRET!, 'base64'))
    .update(msg)
    .digest('base64');
  return {
    'lmts-api-key':   process.env.LMTS_TOKEN_ID!,
    'lmts-timestamp': ts,
    'lmts-signature': sig,
  };
}

async function fetchFills() {
  const res   = await fetch(`${BASE}/portfolio/trades`, { headers: signedHeaders('GET', '/portfolio/trades') });
  const fills = await res.json();                        // flat array, AMM trades only

  console.log(`Fetched ${fills.length} fills`);
  for (const f of fills) {
    console.log(
      f.blockTimestamp,                                  // ISO 8601
      f.market.slug,
      f.strategy,                                        // 'Buy' or 'Sell'
      `amount=${f.outcomeTokenAmount}`,
      `price=${f.outcomeTokenPrice}`,
    );
  }
}

fetchFills().catch(console.error);

How to run this

  1. Set LMTS_TOKEN_ID + LMTS_TOKEN_SECRET (your scoped HMAC token). This snippet uses raw fetch/aiohttp/net/http because no SDK method wraps /portfolio/trades yet.
  2. Save the snippet above as fetch-fills.ts, then run npx tsx fetch-fills.ts.
  3. Each fill prints with its timestamp, market slug, strategy (Buy or Sell), amount, and execution price, your full AMM trading history in one feed.

Section 03

Computing PnL.

PnL splits into two numbers and you need both. Realised is cash that already moved, you sold shares, the USDC hit your balance, the number can’t change. Unrealised is what your open positions would be worth if you closed them right now at the current mark, it moves every time the book does. Treat the first as your track record and the second as live exposure. The two cards below define each precisely; the code under them computes both from the endpoints you already know.

Realised PnL

Sum of (exit price − entry price) × closed size, minus fees. Comes entirely from the fills stream. Deterministic: replay the fills and you get the same number every time.

Unrealised PnL

Open size × (current mark − average entry). Needs a live mark price from the order book or mid. Moves tick by tick, so quote it alongside a timestamp.

// Module 06, Computing PnL
// Realised from fills, unrealised from open positions + live marks.

import { createHmac } from 'node:crypto';
import { HttpClient, PortfolioFetcher, MarketFetcher } from '@limitless-exchange/sdk';

const BASE = 'https://api.limitless.exchange';

function signedHeaders(method: string, pathWithQuery: string, body = ''): Record<string, string> {
  const ts  = new Date().toISOString();
  const msg = `${ts}\n${method}\n${pathWithQuery}\n${body}`;
  const sig = createHmac('sha256', Buffer.from(process.env.LMTS_TOKEN_SECRET!, 'base64'))
    .update(msg)
    .digest('base64');
  return {
    'lmts-api-key':   process.env.LMTS_TOKEN_ID!,
    'lmts-timestamp': ts,
    'lmts-signature': sig,
  };
}

const httpClient = new HttpClient({ apiKey: process.env.LIMITLESS_API_KEY });
const portfolio  = new PortfolioFetcher(httpClient);
const markets    = new MarketFetcher(httpClient);

async function computePnL() {
  // Fills via raw HTTP (no SDK method for GET /portfolio/trades).
  const res       = await fetch(`${BASE}/portfolio/trades`, { headers: signedHeaders('GET', '/portfolio/trades') });
  const fills     = await res.json();
  const positions = await portfolio.getPositions();

  // Realised: net collateral flow across all fills.
  const realised = fills.reduce((acc: number, f: any) => {
    const flow = Number(f.collateralAmount) / 1e6;       // USDC 6 decimals
    return acc + (f.strategy === 'Sell' ? flow : -flow);
  }, 0);

  // Unrealised: (marketValue - cost) per CLOB position.
  let unrealised = 0;
  for (const p of positions.clob) {
    const yesCost  = Number(p.positions.yes.cost) / 1e6;
    const yesValue = Number(p.positions.yes.marketValue) / 1e6;
    if (yesCost === 0) continue;
    unrealised += yesValue - yesCost;
  }

  console.log('realised  :', realised.toFixed(2));
  console.log('unrealised:', unrealised.toFixed(2));
  console.log('total     :', (realised + unrealised).toFixed(2));
}

computePnL().catch(console.error);

How to run this

  1. Keep LIMITLESS_API_KEY (the SDK call) and LMTS_TOKEN_ID + LMTS_TOKEN_SECRET (the raw signed request) set. You’ll need at least one real fill on your account to see a non-zero realised number; otherwise both figures round to zero.
  2. Save the snippet above as compute-pnl.ts, then run npx tsx compute-pnl.ts.
  3. The script prints three lines, realised, unrealised, and total, each in USDC, rounded to two decimal places.

Section 04

Reconciling with on-chain state.

The chain is the source of truth

The REST API is a fast, cached view of your account. The chain is what actually holds your tokens. When the two disagree, which happens during reorgs, backfill lag, or provider hiccups, the chain wins.

  • Reconcile tokensBalance on each CLOB position against the balanceOf call on the outcome token contract
  • Cross-check USDC balance against the Negrisk / CTF adapter on Base
  • Run the reconciliation on a schedule (every 5 minutes) and alert on mismatch
  • Treat any mismatch > dust as a hard stop, kill new orders, page a human

Mental model: the REST positions endpoint is your P&L and UX layer. balanceOf on-chain is your risk layer. Never let the two drift without noticing.

Wire positions into your control panel.

The seed positions.json you dropped in Module 02 was hand-written. As soon as your code from this module pulls real positions, write the array to $ACADEMY_DATA_DIR/positions.json on every refresh. The positions table in your panel re-renders against the new file on the next poll, one source of truth, two surfaces (panel for the laptop, raw JSON for any other consumer you wire later).

Common questions

Positions and PnL: what people ask

Each answer also ships invisibly as schema.org FAQ data for search engines and AI assistants. Tap a question to expand.

  1. What is the difference between /portfolio/trades and /portfolio/history?
    GET /portfolio/trades returns a flat array in one call, no pagination, but AMM trades only; a CLOB-heavy bot reconciling realised PnL from it silently misses every limit-order fill and under-reports its book. GET /portfolio/history is the cursor-paginated endpoint (cursor + limit) covering full activity: CLOB fills, splits, merges, and claims; loop while nextCursor is non-null. Cache locally in NDJSON keyed by txHash so reruns only fetch new rows.
  2. Why does my bot show $0.00 on AMM position rows?
    Decimals. USDC has 6 decimals; outcome tokens have 18. Dividing cost and marketValue by 1e6 is correct for USDC, but the same fields on AMM positions hold outcome-token amounts, so reusing the divisor shows $0.00 next to right-looking CLOB numbers, no error, no warning. Build a fromUnits(amount, decimals) helper (6 for USDC, 18 for outcome tokens) and never inline / 1e6.
  3. Why don’t positions update right after placing an order?
    The REST /portfolio/positions response is assembled by an indexer, not by querying the chain on every call, and indexers lag fills by usually 1–5 seconds, occasionally longer during reorgs. A bot that places an order and reads positions on the next line computes PnL against pre-fill state, double-places, or trips a risk check on phantom exposure. Treat the order ack as truth: update local position state synchronously, and never use getPositions() as a read-after-write primitive.
  4. What headers do authenticated Limitless portfolio calls need?
    Three HMAC headers: lmts-api-key, lmts-timestamp, and lmts-signature, a base64 HMAC-SHA256 over the timestamp, method, path, and body, computed from your token secret (LMTS_TOKEN_SECRET). The legacy X-API-Key header is deprecated. No SDK method wraps /portfolio/trades yet, so the module signs raw HTTP requests with exactly these headers; getPositions() handles the signing for you.
  5. How do you compute realised and unrealised PnL from the API?
    Realised: sum of (exit price − entry price) × closed size, minus fees, entirely from the fills stream, so it’s deterministic and replayable. Unrealised: open size × (current mark − average entry), which needs a live mark from the order book or mid, so quote it alongside a timestamp. The module’s working version nets collateralAmount flows across fills for realised and takes marketValue minus cost per CLOB position for unrealised.

Section 05

Module checklist.

Tick each item once you’ve actually done it. The Continue button unlocks at 5/5.

Foundations tier wrap-up

Take it live.

You’ve got the four foundations: auth, markets, orders, and positions. The next thing your code can teach you is what real fills feel like. Open a small position with what you’ve built, the rest of the curriculum makes more sense once your bot has touched the live book.

Start trading on Limitless

Tier 1 complete · Foundations

Foundations complete.

Your bot can now answer the only questions a trader actually cares about: what do I hold right now, what is it worth, and is the exchange and the chain telling me the same story? With those answered, you have the floor a real strategy can stand on.

Concretely, you can read markets, place orders, and track your portfolio from code. Three things you walk away with at the end of the Foundations tier:

01

A single call that returns your entire live portfolio, CLOB + AMM positions, accumulated points, and reward balance, ready to feed into a dashboard or a risk check.

02

A fills stream from /portfolio/trades and the realised-PnL formula that turns it into a hard number, deterministic, replayable, audit-ready.

03

The reconciliation mental model every production bot needs, REST is your UX layer, on-chain balanceOf is your risk layer, and the two must agree.

Quick recall

Without scrolling back, can you answer these?

Five questions across the API basics tier. Click each to reveal, the test is whether you can answer first.

  1. What’s the difference between authenticating a GET request and signing a transaction with your wallet?
    An authenticated GET proves your identity to the server, the API key says “this request came from you.” A wallet-signed transaction proves your intent to the chain, your private key signs a payload that the contract can verify on-chain. Reads need only the API key. Orders need both: the key gets the request through; the signature lets the chain accept the trade.
  2. Two markets quote the same mid-price. Why might one fill you 5% worse than the other?
    Order book depth. Mid-price is just the average of best bid and best ask, it says nothing about how much liquidity sits at each level. A market with $50 at the top of book and a thin spread will eat into the next level on any size. Walk the book one or two levels past your size before claiming the mid is your fill.
  3. You place a limit-buy at $0.55 for 100 shares. The mid is $0.54. Will you fill at $0.55?
    Probably not. A limit-buy at $0.55 says “buy at $0.55 or better.” If sellers sit at $0.54, you fill there first. If the book lifts to $0.56 before your order rests, you don’t fill at all, the limit caps the price you’ll accept, not the price you’ll pay. Effective fill price is whatever the resting book shows, capped by your limit.
  4. Realised vs unrealised PnL, which one moves with the order book, and why does that matter for risk?
    Unrealised. Realised PnL is closed cash, once a fill clears, that number is locked. Unrealised is open size × (mark − avg entry) and the mark moves tick by tick. For risk, that means realised is your track record (deterministic, replayable from fills) while unrealised is your live exposure (must be re-quoted every refresh, always with a timestamp). Mixing them is how dashboards lie.
  5. Why is the chain the source of truth, not the REST API, and what should your bot do when they disagree?
    REST is a fast, indexed view. The chain is what actually holds your tokens. During reorgs, indexer lag, or provider hiccups, the two diverge. When that happens, balanceOf on-chain wins. Reconcile every few minutes; treat any non-dust mismatch as a hard stop, kill new orders, page a human, do not assume it’ll resolve itself. The size of the divergence is the size of your potential loss.

Next up: real-time. Websockets, order-book streams, rate limits, and the error-handling patterns that keep a long-running bot alive when the network hiccups.

Complete the checklist above to unlock