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
/v1/collections/{id}/salescurl "https://exclusivo.one/v1/collections/solgods_/sales?limit=2"{
"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:
{
"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
| Parameter | In | Default | Notes |
|---|---|---|---|
id | path | required | Canonical slug, contract address or collection mint, or a <chain>_<contract> document id. |
cursor | query | none | The pagination.nextCursor from your previous page. Scoped to this endpoint; a listings cursor is rejected as bad_request. |
limit | query | 25 | Page 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
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.
| Field | Type | Notes |
|---|---|---|
chain | string | Resolved from the sale record, falling back to the collection chain. Decides how price.raw is scaled. |
contractAddress | string | null | EVM contract, lowercase. Null on Solana rows. |
tokenId | string | null | EVM token id as a decimal string. Null on Solana rows, and occasionally null on EVM rows — see below. |
mint | string | null | Solana asset mint, base58 case preserved. Null on EVM rows. |
collectionId | string | Canonical collection document id. Opaque. |
price | Price | The amount the sale settled at. Whether buyer-side fees are inside this number depends on the source — see the gotchas. |
seller | string | null | The wallet that sold the asset. |
buyer | string | null | The wallet that bought it. Populated on this endpoint, unlike listings. |
marketplace | string | "EXC" for Exclusivo-native sales; otherwise the source slug, e.g. "opensea" or "magic-eden" for an aggregated purchase. |
status | string | Always "sold" on this endpoint. |
expiresAt | null | Always null. The field exists because listings share this shape. |
timestamp | string (ISO 8601) | When the sale was recorded. This is the field the feed sorts on. |
orderId | string | null | The listing this sale filled, where one is recorded — the join key back to a row you previously saw on the listings endpoint. |
tx | string | null | Solana 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.
// 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
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
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.