Guides

Verify listings on-chain

If you would rather not take our API's word for it, you do not have to. Every Exclusivo sale leaves a marker in the transaction itself. This guide shows how to pull one and confirm it independently — and is equally clear about the cases where chain data alone is not enough.

What this proves

We do not run a proprietary settlement contract. EVM sales settle on canonical OpenSea Seaport; Solana sales settle through transfers involving our escrow wallet. So "is this an Exclusivo sale?" is a question about markers inside an otherwise ordinary transaction, and the answer is different per chain.

ChainMarkerWhat it proves
EVMSeaport OrderFulfilled.zoneThe filled order was built by Exclusivo. Parties, price, fee and royalty all decode from the same log.
SolanaSPL Memo prefixed exclusivo:v1:Combined with escrow-wallet involvement in the same transaction: the action, its parties, and its economics.

Both conventions apply to activity from 2026-07-26 onward. Everything you need is readable from a public RPC endpoint — no key, and no call to us.

EVM: decode the Seaport log

Every Seaport order Exclusivo builds sets its zone to our platform treasury address. The order stays fully open, so the zone has no execution authority — it is purely an attribution marker, and Seaport echoes it in the OrderFulfilled log.

ConstantValue
Seaport 1.50x00000000000000ADc04C56Bf30aC9d3c0aAF14dC
Seaport 1.60x0000000000000068f116a894984e2db1123eb395
Exclusivo zone0xCD93552140bF932842Dbc767D0c6Cc75F2BA680C
OrderFulfilled topic00x9d9af8e38d66c62e2c12f0225249fd9d721c54b83f48d9352c97c6cacdcb6f31

Step 1 — get a transaction hash

Take one from a sale row in the API (the tx field) so you can check our claim against the chain, or from any wallet's history if you have no starting point.

curl
curl "https://exclusivo.one/v1/collections/{id}/sales?limit=1"
# → data[0].tx        the transaction hash
# → data[0].orderId   the Seaport order hash we claim it filled

Step 2 — pull the receipt and find our log

Fetch the receipt from any RPC provider, keep only logs emitted by a canonical Seaport deployment, decode them as OrderFulfilled, and compare the zone. Address comparisons are case-insensitive — lowercase both sides.

JavaScript (viem)
import { createPublicClient, http, parseAbiItem, decodeEventLog } from "viem";
import { mainnet } from "viem/chains";

const SEAPORT = new Set([
  "0x00000000000000adc04c56bf30ac9d3c0aaf14dc", // 1.5
  "0x0000000000000068f116a894984e2db1123eb395", // 1.6
]);
const EXCLUSIVO_ZONE = "0xcd93552140bf932842dbc767d0c6cc75f2ba680c";

const ORDER_FULFILLED = parseAbiItem(
  "event OrderFulfilled(bytes32 orderHash, address indexed offerer, address indexed zone, address recipient, (uint8 itemType, address token, uint256 identifier, uint256 amount)[] offer, (uint8 itemType, address token, uint256 identifier, uint256 amount, address recipient)[] consideration)"
);

const client = createPublicClient({ chain: mainnet, transport: http(RPC_URL) });
const receipt = await client.getTransactionReceipt({ hash });

const fills = [];
for (const log of receipt.logs) {
  if (!SEAPORT.has(log.address.toLowerCase())) continue;

  let decoded;
  try {
    decoded = decodeEventLog({ abi: [ORDER_FULFILLED], data: log.data, topics: log.topics });
  } catch {
    continue; // some other Seaport event
  }

  // The whole attribution check is this one comparison.
  if (decoded.args.zone.toLowerCase() !== EXCLUSIVO_ZONE) continue;
  fills.push(decoded.args);
}

Step 3 — decode parties and economics

For a filled listing, the offer is the NFT and the consideration is the money. The offerer is the seller and the recipient is the buyer. Split the consideration by recipient: what goes to the offerer is the seller's proceeds, what goes to our treasury is the marketplace fee, and anything left over is the royalty.

JavaScript
const sum = (items) => items.reduce((total, item) => total + item.amount, 0n);

function decodeListingFill(args) {
  const { orderHash, offerer, recipient, offer, consideration } = args;

  // itemType 2 = ERC721, 3 = ERC1155
  const nft = offer.find((item) => item.itemType === 2 || item.itemType === 3);

  const seller = offerer.toLowerCase();
  const toSeller = consideration.filter((c) => c.recipient.toLowerCase() === seller);
  const toPlatform = consideration.filter((c) => c.recipient.toLowerCase() === EXCLUSIVO_ZONE);
  const toRoyalty = consideration.filter(
    (c) => !toSeller.includes(c) && !toPlatform.includes(c)
  );

  const price = sum(toSeller);            // seller ask = seller proceeds
  const marketplaceFee = sum(toPlatform); // 200 bps of price
  const royaltyAmount = sum(toRoyalty);

  return {
    orderId: orderHash,
    collection: nft.token.toLowerCase(),
    tokenId: nft.identifier.toString(),
    seller,
    buyer: recipient.toLowerCase(),
    // Seaport ItemType: 0 = NATIVE, 1 = ERC20, 2 = ERC721, 3 = ERC1155.
    // Only 0 is native; 1 carries the token address (WETH on accepted offers).
    currency:
      consideration[0].itemType === 0 ? "native" : consideration[0].token.toLowerCase(),
    price: price.toString(),
    marketplaceFee: marketplaceFee.toString(),
    royaltyAmount: royaltyAmount.toString(),
    royaltyRecipient: toRoyalty[0]?.recipient.toLowerCase() ?? null,
  };
}

// Sanity check: our EVM marketplace fee is exactly 200 bps of the ask.
const fill = decodeListingFill(fills[0]);
const bps = (BigInt(fill.marketplaceFee) * 10000n) / BigInt(fill.price);
console.assert(bps === 200n, "unexpected fee ratio", bps);

Accepted collection offers invert

When the fill is an accepted offer rather than a bought listing, the offer item is the currency and the consideration contains the NFT, so offerer is the buyer. The full inversion rule is on EVM — Seaport attribution.

Step 4 — close the loop against the API

The orderHash you decoded is the same value the API returns as orderId on that sale. If the two match, the row we published and the transaction you just decoded are the same event, and every number in it came from the chain rather than from us.

One further marker exists but is not visible in logs: every order we build carries a constant 8-byte brand prefix 0x455843313a534561 (ASCII EXC1:SEa) in the top bytes of its salt. It brands the signed order struct itself, so it is checkable against raw order JSON or fulfilment calldata, not against a receipt.

Solana: decode the memo

There is no marketplace program on Solana. Our actions are transfers involving the platform escrow wallet, and each marketplace transaction carries exactly one SPL Memo instruction whose payload is exclusivo:v1: followed by compact JSON.

ConstantValue
Memo programMemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr
Memo prefixexclusivo:v1:
Escrow walletHUxx919u8CSpyzLcmDYvBrvzBNt7caCdgDv2Ucjm4fge

Step 1 — fetch the transaction

Start from the tx field of an API sale row, or from a signature in the asset's history. Request the parsed form so the memo arrives as a string.

JavaScript (@solana/web3.js)
import { Connection } from "@solana/web3.js";

const connection = new Connection(RPC_URL, "confirmed");
const tx = await connection.getParsedTransaction(signature, {
  maxSupportedTransactionVersion: 0,
});

Step 2 — find the memo and check the prefix

Scan the instructions for the SPL Memo program. Anything that does not begin with the prefix is not one of our events.

JavaScript
const MEMO_PROGRAM_ID = "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr";
const PREFIX = "exclusivo:v1:";

const memoIx = tx.transaction.message.instructions.find(
  (ix) => ix.programId.toBase58() === MEMO_PROGRAM_ID
);
const memo = typeof memoIx?.parsed === "string" ? memoIx.parsed : null;
if (!memo?.startsWith(PREFIX)) throw new Error("not an Exclusivo marketplace transaction");

const event = JSON.parse(memo.slice(PREFIX.length));
// e.g. { t: "sale", lid, m, c, p, cur, s, b, f, r, rt, std }

Step 3 — confirm the escrow wallet is involved

This step is what makes the check meaningful. A memo is unauthenticated text; anyone can write our prefix into one. What cannot be forged is our escrow wallet signing or being a counterparty to the transfers in the same transaction.

JavaScript
const ESCROW = "HUxx919u8CSpyzLcmDYvBrvzBNt7caCdgDv2Ucjm4fge";

const keys = tx.transaction.message.accountKeys;
const escrowKey = keys.find((k) => k.pubkey.toBase58() === ESCROW);
if (!escrowKey) throw new Error("escrow wallet not involved — the memo alone proves nothing");

// escrow signs the release / cancel legs; the buyer signs the payment leg.
const escrowSigned = Boolean(escrowKey.signer);

Never lowercase a base58 address

Solana addresses are case-sensitive. Comparing them case-insensitively, or normalizing them the way you would an EVM address, produces a different and invalid address.

Step 4 — check the economics against the balance changes

The buyer pays fees on top of the ask, so the total moved is p + f + r. Cross-check the memo against the transaction's balance deltas:

JavaScript
function lamportDelta(tx, address) {
  const index = tx.transaction.message.accountKeys.findIndex(
    (k) => k.pubkey.toBase58() === address
  );
  if (index < 0) return null;
  return BigInt(tx.meta.postBalances[index]) - BigInt(tx.meta.preBalances[index]);
}

// On a "sale" / "sale_pay", the seller should receive exactly the recorded ask.
const sellerDelta = lamportDelta(tx, event.s);
console.assert(sellerDelta === BigInt(event.p), "seller proceeds do not match the memo");

Prefer the memo over summing transfer legs when the two disagree on the fee/royalty split: creator payouts below the rent-exempt minimum are folded into the escrow leg, so the legs understate the royalty while the memo records the intended split.

Step 5 — join two-transaction sales

Core and compressed sales settle as two transactions: a buyer-signed payment (t: "sale_pay") and a later escrow-signed release (t: "sale_release"). Both memos carry the same lid. Take price and parties from the payment, and treat the release as the point at which the sale is final. Release transactions are retried with fresh blockhashes, so deduplicate by lid rather than by signature.

What you cannot verify

Being honest about the gaps is more useful than pretending they do not exist. None of the following is reconstructible from chain data, at any level of effort:

CaseWhy notWhere to get it
EVM listingsA listing is an off-chain signed Seaport order in our database. No transaction is ever sent./v1 listings
EVM cancelsCancelling removes the order from our book without calling Seaport, so nothing is emitted. The signature itself remains technically fillable until it expires./v1 listings
Orders created before 2026-07-26They carry zone = address(0). The only signal left is the weak heuristic of a 200 bps consideration leg paid to our treasury./v1 sales
ApeChain activityApeChain fills route through Reservoir rather than Seaport, so no zone attribution exists./v1 sales
External aggregated buysBuying a foreign OpenSea listing through our interface is an OpenSea sale, not an Exclusivo one. Our 2% aggregator fee is a bare transfer in a separate transaction and emits no log.Not applicable — do not attribute these to us.
Solana history before 2026-07-26Older transactions carry unstructured prose memos with no ids or amounts./v1 sales

Absence of a memo is not absence of an event

When a Solana transaction would not otherwise fit inside the transaction size limit, the memo degrades to a minimal payload, and release or return transactions drop it entirely rather than fail settlement over logging. A transfer from the escrow wallet with no memo can still be a real Exclusivo action.

Full field reference

This guide covers the verification path. The complete field-by-field references live in the On-chain section: