Guides
Wallet & portfolio integration
For wallet and portfolio teams: how to attach a credible value and a correct listed state to the NFTs a user already holds, without burning your rate-limit budget and without the escrow custody trap that makes listed Solana NFTs look like they belong to a stranger.
What we provide
We do not expose a "list this wallet's NFTs" endpoint. You already have the user's holdings from your own indexer or from RPC — what we add is pricing, liquidity, and market state for the assets in that list.
| You need | Endpoint | Cache TTL | Rate bucket |
|---|---|---|---|
| Price and status of one NFT | GET /v1/tokens/{chain}/{address}/{tokenId} | 30s | item (300/min) |
| Collection floor | GET /v1/collections/{id}/floor | 10s | item (300/min) |
| Collection identity and stats | GET /v1/collections/{id} | 30s | item (300/min) |
All three sit in the 300 requests-per-minute item bucket, which is the generous one. The 120/min list bucket only matters if you also page listings or sales. No key is required, and responses are CORS-open, so a client-side wallet can call them directly.
1. Price a single token
The token endpoint is the per-asset primitive. The path parameters differ by chain, and getting them the wrong way round is the most common first mistake:
| Chain | {address} | {tokenId} |
|---|---|---|
| EVM | The contract address (any casing; we lowercase it). | The token id, as a decimal string. |
| Solana | The COLLECTION mint, base58, case-preserved. | The ASSET mint, base58, case-preserved. |
# EVM
curl "https://exclusivo.one/v1/tokens/base/0x1234…abcd/1782"
# Solana — collection mint, then asset mint
curl "https://exclusivo.one/v1/tokens/solana/85Ghq5ExuySXEU4aExGs7d44udeGrF3bvF6GX8YsgaMe/9xQe…"{
"data": {
"chain": "base",
"contractAddress": "0x1234…abcd",
"tokenId": "1782",
"mint": null,
"collectionId": "base_0x1234…abcd",
"name": "Example #1782",
"image": "https://…",
"rarity": { "rank": 412 },
"owner": { "address": "0xabcd…1234", "kind": "wallet", "label": null },
"listing": {
"price": { "raw": "45000000000000000", "decimal": 0.045, "currency": "ETH", "usd": 112.5 },
"seller": "0xabcd…1234",
"marketplace": "EXC",
"status": "active",
"expiresAt": "2026-08-09T12:00:00.000Z",
"orderId": "0x9f3c…",
"tx": null
},
"lastSale": { "price": { "raw": "38000000000000000", "decimal": 0.038, "currency": "ETH", "usd": 95 } }
}
}Batching and budget
There is no batch variant, so pricing N tokens costs N requests. At 300 requests per minute in the item bucket, that is comfortable for one user's wallet and uncomfortable if you fan out across many users from one egress IP. Two rules keep you inside the budget:
- Value by floor first, per token second. One
/floorcall covers every NFT the user holds in that collection. A 200-NFT wallet spread over 25 collections costs 25 requests to value, not 200. - Fetch per-token detail lazily. Call the token endpoint for the assets the user actually opens, for anything you need to show as listed, and for high-rarity items where the floor is a poor estimate.
Cache on your side for at least the endpoint's TTL (10s for floors, 30s for tokens); requesting faster than that returns the same cached body while still spending budget. Keep concurrency modest — four to six in flight — and honour Retry-After on a 429.
2. Which floor to show
"Floor" is ambiguous, so we return both numbers explicitly rather than picking one for you. Our marketplace fee and royalties are charged to the buyer on top of the seller's ask, which means the two figures are genuinely different.
| Field | What it is | Show it when |
|---|---|---|
askFloor | The cheapest seller ask. Because fees are buyer-side, this is exactly what a seller receives. | Valuing what the user holds, and comparing our floor with another marketplace’s listed floor. |
buyerFloor | The all-in cost of the cheapest fill — ask plus marketplace fee plus royalty, or the external venue’s buyer total. | Any “buy the floor” action, or anywhere you quote what the user will actually pay. |
curl "https://exclusivo.one/v1/collections/solgods_/floor"{
"data": {
"collectionId": "solana_85Ghq5…",
"askFloor": { "raw": "12500000000", "decimal": 12.5, "currency": "SOL", "usd": 2437.5 },
"buyerFloor": { "raw": "12937500000", "decimal": 12.937, "currency": "SOL", "usd": 2522.8 },
"aggregated": true,
"updatedAt": "2026-07-26T10:31:02.114Z",
"stale": false
}
}aggregated is always true: the floor spans Exclusivo-native listings and the external marketplaces we aggregate, so it is a market floor rather than a venue-specific one. stale: true means the live resolve failed and buyerFloor came from the last-good stored value — still worth showing, but do not present it as a live quote. Either floor can be null when a collection has no active listings; render an empty state rather than zero.
Do not mix the two
buyerFloor overstates it by the entire fee stack, and comparing our buyerFloor against another marketplace's ask floor makes us look more expensive than we are. Compare like for like.3. Show listed state correctly
A token is listed if and only if listing is non-null. Expired listings are filtered out server-side, so anything you receive is live at the time of the response.
listing.marketplacenames the venue.EXCmeans the NFT is listed on Exclusivo; any other value is a listing we surface from an external venue such asopenseaormagic-eden. If your UI offers a "manage listing" action, only offer it forEXC.listing.priceis the seller ask. Showraw-derived values for anything financial. If you want to tell the user what they would net, the ask is the net.listing.expiresAtcan be null. Null means no expiry was recorded, not that the listing expires now.- Solana listing state can lag by a couple of minutes. Our Solana listings reach the read path through an indexer mirror that syncs roughly every two minutes, so an NFT the user just listed may briefly still read as unlisted. Treat your own optimistic state as authoritative for that window.
4. The beneficial-owner gotcha
This one silently breaks portfolio views, so it is worth reading even if you skip the rest. Listing an NFT on Solana deposits it into a shared platform escrow wallet. On-chain, the owner of that NFT is no longer the user — it is a custody address that holds every listed NFT on the platform simultaneously.
If you render raw chain ownership, the consequences are:
- The user's listed NFTs vanish from their portfolio the moment they list them.
- The same address appears as the "owner" of thousands of unrelated NFTs, which looks like a whale wallet and is not one.
- Any "owned by" label you show is wrong in a way that is impossible for the user to interpret.
The token endpoint resolves this for you. owner is the beneficial owner: while a token is listed, that is the seller, never the custody wallet.
| owner.kind | Meaning | Render as |
|---|---|---|
wallet | A real user wallet. owner.address is populated. | The address / profile as normal. |
custody | The asset sits in a shared custody wallet and no beneficial owner could be resolved. owner.address is deliberately null; owner.label describes the custodian. | A neutral state such as "in escrow" using owner.label — never an address. |
unknown | Ownership could not be determined. | An empty state. Do not fall back to the raw chain owner. |
Never substitute the raw on-chain owner
Worked example: value a wallet
Holdings come from your side; we supply the prices. This values by collection floor, then upgrades the estimate for the assets that are actually listed, and keeps the totals per chain because native currencies are not addable.
const BASE = "https://exclusivo.one/v1";
async function get(path) {
const res = await fetch(BASE + path, { headers: { accept: "application/json" } });
if (res.status === 429) {
const wait = Number(res.headers.get("retry-after")) || 2;
await new Promise((r) => setTimeout(r, wait * 1000));
return get(path);
}
if (!res.ok) throw new Error((await res.json()).error.message);
return res.json();
}
/**
* holdings: [{ chain, collectionAddress, tokenId }]
* EVM → collectionAddress = contract, tokenId = decimal token id
* Solana → collectionAddress = collection mint, tokenId = asset mint
*/
async function valueHoldings(holdings) {
// 1. One floor lookup per collection, not per NFT.
const collections = new Map();
for (const h of holdings) {
const key = `${h.chain}:${h.collectionAddress}`;
if (!collections.has(key)) collections.set(key, []);
collections.get(key).push(h);
}
const floors = new Map();
for (const [key, [first]] of collections) {
const { data } = await get(`/collections/${first.collectionAddress}/floor`);
// askFloor is the seller-side number — what these NFTs are worth to the holder.
floors.set(key, { price: data.askFloor, stale: data.stale });
}
// 2. Per-chain subtotals in native units; USD only when every leg has a rate.
const byChain = new Map();
const items = [];
for (const [key, group] of collections) {
const floor = floors.get(key);
for (const h of group) {
const chain = h.chain;
const subtotal = byChain.get(chain) ?? { native: 0, currency: null, usd: 0, usdComplete: true };
if (floor.price) {
subtotal.native += floor.price.decimal;
subtotal.currency = floor.price.currency;
if (floor.price.usd == null) subtotal.usdComplete = false;
else subtotal.usd += floor.price.usd;
}
byChain.set(chain, subtotal);
items.push({ ...h, estimate: floor.price, estimateStale: floor.stale });
}
}
return { items, byChain };
}
/** Upgrade one item from a floor estimate to its real market state. */
async function enrich(item) {
const { data } = await get(`/tokens/${item.chain}/${item.collectionAddress}/${item.tokenId}`);
return {
...item,
name: data.name,
image: data.image,
rarityRank: data.rarity.rank,
// Beneficial owner — for a listed Solana NFT this is the seller, NOT the
// shared escrow wallet that holds the asset on-chain.
owner: data.owner,
listed: data.listing != null,
listPrice: data.listing?.price ?? null,
listedOn: data.listing?.marketplace ?? null,
lastSale: data.lastSale?.price ?? null,
};
}For a 200-NFT wallet across 25 collections this costs 25 requests up front, plus one per asset the user opens. If you show a "cost to buy the floor" figure anywhere in the same view, take it from buyerFloor in the response you already fetched rather than making a second call.
What to watch out for
- Do not sum native currencies across chains. SOL and ETH subtotals are not addable.
price.usdis best-effort and is null whenever the feed is unavailable, and always null for chains without one — so a single-number portfolio total must be labelled as partial when any leg is missing. - A floor estimate is not a quote. Rare items are worth more than the floor, and a collection with one thin listing has a floor that does not survive contact with a seller. Label floor-derived values as estimates.
- Solana base58 is case-sensitive. Do not lowercase mints or wallet addresses. EVM addresses come back lowercase and can be compared case-insensitively.
- Handle 404 as "not indexed here". A token we have never seen returns
404 not_found. That is not an error state for the user — fall back to your own metadata and omit the price. - Retry 502s.
upstream_errormeans a data source is briefly unavailable. Do not cache it as a zero value.
Field-by-field details for both endpoints are in Tokens and Floor; shared behaviour such as pagination, errors and rate limits is in Conventions.