API reference

Sales

Completed sales for one collection, newest first, in the same cross-chain item shape as listings. Solana signatures and EVM transaction hashes both land in the same field.

Endpoint

GET/v1/collections/{id}/sales
curl
curl "https://exclusivo.one/v1/collections/solgods_/sales?limit=2"
Response
{
  "data": [
    {
      "chain": "solana",
      "contractAddress": null,
      "tokenId": null,
      "mint": "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM",
      "collectionId": "solana_85ghq5exuysxeu4aexgs7d44udegrf3bvf6gx8ysgame",
      "price": { "raw": "12500000000", "decimal": 12.5, "currency": "SOL", "usd": 2437.5 },
      "seller": "HN7cABqLq46Es1jh92dQQisAq662SmxELLLsHHe4YWrH",
      "buyer": "5FHwkrdxkjqQhLwoDdRQ4hd1gz4bmL7cUHrz1yfEcvsN",
      "marketplace": "EXC",
      "status": "sold",
      "expiresAt": null,
      "timestamp": "2026-07-26T11:18:52.000Z",
      "orderId": "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM_1785396112",
      "tx": "5VfydnLu4XwV2FQm8sVJKPbLuJqLTMKzZJEcnBAxTNjM7bqYP8gfEwqDfxjRWc3nHtGyQdV6uZpKk9rSXt2eAhWB"
    }
  ],
  "pagination": {
    "nextCursor": "eyJrIjoic2FsZS1kb2MiLCJ2Ijoic2xkOTJraDNtcCJ9",
    "hasMore": true
  }
}

An EVM sale from the same endpoint carries the other half of the identity fields:

EVM sale
{
  "chain": "ethereum",
  "contractAddress": "0x8a90cab2b38dba80c64b7734e58ee1db38b8992e",
  "tokenId": "4271",
  "mint": null,
  "collectionId": "ethereum_0x8a90cab2b38dba80c64b7734e58ee1db38b8992e",
  "price": { "raw": "2400000000000000000", "decimal": 2.4, "currency": "ETH", "usd": 7920.0 },
  "seller": "0x9c8ff314c9bc7f6e59a9d9225fb22946427edc03",
  "buyer": "0x2f1a05b2e5f0a3c9d78b41c6e0f9a4d3b7c81e60",
  "marketplace": "EXC",
  "status": "sold",
  "expiresAt": null,
  "timestamp": "2026-07-25T20:44:03.000Z",
  "orderId": "ethereum_0x8a90cab2b38dba80c64b7734e58ee1db38b8992e_4271_1785412903117",
  "tx": "0x6f3b2a9d1c8e47b05a2f9d6c3e814b7a0d5f2c9e6b31a84d7f0c25e9b6a3d18f"
}

Addresses, hashes and ids above are illustrative examples. Cached for 30 seconds at the edge with a further 30 seconds of stale-while-revalidate.

Parameters

ParameterInDefaultNotes
idpathrequiredCanonical slug, contract address or collection mint, or a <chain>_<contract> document id.
cursorquerynoneThe pagination.nextCursor from your previous page. Scoped to this endpoint; a listings cursor is rejected as bad_request.
limitquery25Page size, 1–100 inclusive. Out-of-range values are rejected, not clamped.

There are no date-range, marketplace, or wallet filters, and no sort parameter — the order is always newest first by sale time. For a date window, page until the timestamps fall outside it and stop.

Errors: bad_request for a bad parameter or cursor; not_found when the identifier does not resolve, or resolves to a collection with no contract address; rate_limited at 120 req/min; upstream_error when the sales store is unavailable. This endpoint has no degraded mode — it never returns a partial page with a stale flag.

What is and is not included

Sales are read from our own activity record, which is written whenever a sale settles through Exclusivo. That covers Exclusivo-native sales on every chain, and external marketplace purchases that a user routed through our interface — the marketplace field names which.

This is not a complete on-chain sales history

A sale of the same collection that happened entirely on another marketplace, with no involvement from us, is not in this feed. If you are computing collection-wide traded volume or a true last-sale price, you need a chain indexer or the source marketplace's own API as well. What this endpoint gives you authoritatively is Exclusivo's trade history, including the EVM sales whose orders never existed on-chain as listings.

For collection-wide aggregates that do span sources, use stats.volume and stats.sales on the collection detail endpoint, which are computed by the indexer rather than derived from this feed.

The sale item

Identical in shape to a listing, with the buyer-side and settlement fields populated instead of the expiry field.

FieldTypeNotes
chainstringResolved from the sale record, falling back to the collection chain. Decides how price.raw is scaled.
contractAddressstring | nullEVM contract, lowercase. Null on Solana rows.
tokenIdstring | nullEVM token id as a decimal string. Null on Solana rows, and occasionally null on EVM rows — see below.
mintstring | nullSolana asset mint, base58 case preserved. Null on EVM rows.
collectionIdstringCanonical collection document id. Opaque.
pricePriceThe amount the sale settled at. Whether buyer-side fees are inside this number depends on the source — see the gotchas.
sellerstring | nullThe wallet that sold the asset.
buyerstring | nullThe wallet that bought it. Populated on this endpoint, unlike listings.
marketplacestring"EXC" for Exclusivo-native sales; otherwise the source slug, e.g. "opensea" or "magic-eden" for an aggregated purchase.
statusstringAlways "sold" on this endpoint.
expiresAtnullAlways null. The field exists because listings share this shape.
timestampstring (ISO 8601)When the sale was recorded. This is the field the feed sorts on.
orderIdstring | nullThe listing this sale filled, where one is recorded — the join key back to a row you previously saw on the listings endpoint.
txstring | nullSolana transaction signature (base58) or EVM transaction hash (0x-prefixed). Null when the record has no settlement reference.

tx is what makes a sale independently verifiable. See Verify listings on-chain for how to confirm one of these rows against chain data without trusting the API.

Pagination

The cursor encodes the position of the last sale on the page you received. Pass it back verbatim; it is scoped to this endpoint and cannot be replayed elsewhere.

Incremental sync
// Walk newest-first and stop once you reach what you already have.
let cursor = null;
let done = false;

while (!done) {
  const url = new URL("https://exclusivo.one/v1/collections/solgods_/sales");
  url.searchParams.set("limit", "100");
  if (cursor) url.searchParams.set("cursor", cursor);

  const res = await fetch(url);
  if (!res.ok) throw new Error((await res.json()).error.message);
  const { data, pagination } = await res.json();

  for (const sale of data) {
    if (sale.timestamp <= lastSyncedAt) { done = true; break; }
    upsert(sale);            // key on tx + orderId, not on array position
  }

  if (!pagination.hasMore) break;
  cursor = pagination.nextCursor;
}

A short page does not mean the end of the book

Sale records whose price cannot be interpreted are dropped after the page is assembled, so data.length can be smaller than limit — sometimes zero — while hasMore is still true. Loop on hasMore, never on data.length === limit, or you will silently truncate the history at the first malformed row.

Cursors can expire

The cursor refers to a specific sale record. If that record is removed between your pages, the next request returns bad_request rather than guessing a nearby position. Handle it by restarting the walk from the newest page and de-duplicating against what you already stored.

Gotchas

Read price.raw, and read the chain first

The underlying store keeps sale amounts in a single integer field whose unit depends on the chain — lamports for Solana, wei for EVM. The API resolves that for you and price.raw is always correct for the item's chain, with price.currency naming the unit. The mistake to avoid is assuming a single global decimals value across a mixed-chain ingest: a wei amount divided by 109 is off by nine orders of magnitude and will not look obviously wrong in a chart.

tokenId can be null on an EVM sale

EVM sale records store the asset reference in more than one historical format, and where the stored value is a bare contract address rather than a token id, the field is set to null instead of returning something wrong. So an EVM sale can arrive with a contractAddress and no tokenId. Such a row is still a real sale with a real price and a real transaction — do not discard it, but do not use it to key a per-token history either.

Fee treatment is not uniform

For Exclusivo-native sales the recorded amount is the settlement price, and buyer-side fees were charged on top of it. For aggregated external purchases the amount is whatever the source marketplace reported, which may already be a buyer total. There is no field distinguishing the two, so a sale-price series mixing marketplace: "EXC" with external rows is not strictly apples-to-apples. If that matters, filter on marketplace.

A 1970 timestamp means the record had no usable time

When a sale record carries no readable creation time, timestamp falls back to the Unix epoch, 1970-01-01T00:00:00.000Z. It is rare, because the feed sorts on that same time and unreadable records generally do not appear at all, but if one reaches you it will sort to the beginning of time and skew any “first sale” calculation. Treat epoch zero as missing.

Collections without a contract address return 404

The lookup is keyed on the collection's contract address or collection mint. A collection record that resolves but has no such address returns not_found from this endpoint even though /v1/collections/{id} returns it happily. If you see that mismatch, it is a property of the collection record, not of your identifier.