Docs · @marani/guardian

Guardian SDK.

Stop your users from sending tokens that a centralized exchange won't credit.

Request access

There is no public URL and no signup yet. Ask for a key and a host.

The guardian runs as a hosted API. This package is a dependency-free client for it, and needs only fetch (Node 18+, browsers, React Native). It ships as ES modules only.

Send check
Before a send to an exchange, find out whether that exchange accepts this exact Solana token. This covers Token-2022 tokens and deposits that are currently suspended.
Exchange badges
"This is Binance" the moment an address is pasted.
Delisting notices
Exchanges' announced delistings of Solana tokens, matched against your user's holdings on their own device.

Call it from your backend. Any API key shipped inside an extension or app can be extracted. Your wallet calls your backend, and your backend calls the guardian.

Quickstart

Create the client in your backend. Then a badge when the address is pasted, a verdict when the token is chosen, and the delisting list on a timer.

import { createGuardian, isUnavailable, matchDelistings } from '@marani/guardian';

const guardian = createGuardian({
  apiKey: process.env.GUARDIAN_KEY!,
  baseUrl: 'https://<your guardian host>',
});

// 1. The address is pasted: show a badge.
const badge = await guardian.address(to);

// 2. The token is chosen: get the verdict.
const verdict = await guardian.check({ to, mint, symbol });
if (isUnavailable(verdict)) {
  // We couldn't check in time. Say so, and never block on it.
} else {
  show(verdict.level, verdict.message);
  // e.g. "block", "Binance doesn't accept USDG on Solana. …"
  if (verdict.level === 'block') {
    // Disable Send. If verdict.swapTo is set, offer
    // "Swap to {swapTo.symbol} and send" instead.
  }
}

// 3. Delistings, e.g. hourly: fetch the whole list and match it locally.
const list = await guardian.delistings();
if (!isUnavailable(list)) {
  const affected = matchDelistings(list.notices, heldMints);
}

Client options

Option Required Meaning
apiKey yes Your API key.
baseUrl yes Your guardian host, as an http(s) URL.
timeoutMs no The deadline for a whole call, in milliseconds. The default is 4000.
fetch no A fetch to use in place of the global one.

When to call what

Moment Call Notes
The address is pasted address(to) A known exchange wallet answers in under about 150 ms. Any other address means reading its on-chain history, up to about 2 s. An exchange deposit address found that way is cached for every partner; an address we can't identify is read again on each call.
The token is chosen check({ to, mint, symbol? }) mint is the token's mint, or "SOL" for native SOL. symbol is your display symbol; it can only ever soften a verdict, and one longer than 16 characters is ignored.
The user answers our question check({ ..., userMark }) After level: 'ask'. Pass 'not-cex' or an exchange id, and remember the answer per address on your side.
Before signing check again Only if the recipient or the token changed.
Hourly, or on wallet open delistings() then matchDelistings The list is small. Your users' holdings never reach us.

Skip the call for sends to the user's own address. The service covers Solana mainnet only.

The answer

{
  "level": "block",
  "code": "CEX_TOKEN_NOT_LISTED",
  "message": "Binance doesn't accept USDG on Solana. If you send it, it won't be credited.",
  "exchange": "binance",
  "swapTo": { "symbol": "USDC", "mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" },
  "notes": [
    {
      "level": "warn",
      "code": "CEX_HOT_WALLET",
      "message": "This is Binance's own wallet, not a deposit address. Binance credits only what you send to the deposit address it gave you."
    },
    {
      "level": "warn",
      "code": "TOKEN2022_EXCHANGE_CREDIT",
      "message": "Binance may be slow to credit USDG: many exchanges credit Token-2022 tokens by hand."
    },
    {
      "level": "info",
      "code": "TOKEN2022_PERMANENT_DELEGATE",
      "message": "USDG's issuer can move or burn it from any wallet."
    }
  ]
}
Field When Meaning
level always What to do: block, warn, ok or ask.
code always Why, as a code to translate by (table below).
message always Why, as a sentence ready to show your user.
exchange always The exchange id, or null when the address isn't one or we can't tell.
swapTo some blocks The token to swap into and send instead, today USDC.
feePct fee tokens The token's transfer fee, percent.
notes always Anything else worth showing, worst first: {level, code, message}. Often empty.

Listing freshness lives in GET /v1/health (dataAsOf), not on every answer.

The badge

/v1/address answers the recipient alone: { "exchange": "kucoin", "addressType": "hot-wallet" }, where addressType is deposit or hot-wallet (the exchange's own wallet). An unknown address is { "exchange": null }, with "likelyExchange": true added when it is probably an exchange deposit address.

Levels: what to show

Level Meaning Recommended UX
block The exchange will not credit this deposit. Disable Send and show message. Offer "Swap to {swapTo.symbol} and send" when swapTo is present.
warn A real risk, or something we couldn't confirm. Show message and ask the user to confirm.
ok No problems found. Proceed. Show any info notes quietly, if at all.
ask We couldn't tell whether the address is an exchange: the usual answer for a personal wallet. Show message (it is the question) with a list of exchanges and "No, a personal wallet", then call check again with the answer as userMark. A lighter prompt is your call.
unavailable Comes from the client, not the service: a timeout, a 429 or a 5xx. "Couldn't check". Never block on it.

Codes

message is ready in English. For other languages, translate by code, on the answer and on each note:

Code Level Our English
CEX_TOKEN_NOT_LISTED block {exchange} doesn't accept {symbol} on Solana. If you send it, it won't be credited.
CEX_TOKEN_NOT_LISTED warn {exchange} may not accept {symbol} on Solana. Check their deposit page before sending.
CEX_DEPOSITS_SUSPENDED block {exchange} has {symbol} deposits switched off right now. A send could take days to credit, or get stuck.
CEX_TOKEN_SUPPORTED ok {exchange} accepts {symbol} on Solana.
CEX_TOKEN_SUPPORTED warn {exchange} appears to accept {symbol}. Confirm the token address on their deposit page.
CEX_NO_DATA warn We can't confirm {exchange} accepts {symbol} on Solana. Check their deposit page before sending.
CEX_HOT_WALLET warn This is {exchange}'s own wallet, not a deposit address. {exchange} credits only what you send to the deposit address it gave you.
TOKEN2022_EXCHANGE_CREDIT warn {exchange} may be slow to credit {symbol}: many exchanges credit Token-2022 tokens by hand.
TOKEN2022_TRANSFER_FEE warn {symbol} takes a {feePct}% fee on every transfer, so the recipient gets less than you send.
TOKEN2022_TRANSFER_HOOK warn {symbol} runs extra code on every transfer. Some exchanges and wallets don't support that.
TOKEN2022_PERMANENT_DELEGATE info {symbol}'s issuer can move or burn it from any wallet.
DESTINATION_UNKNOWN ask Is this an exchange deposit address? We couldn't tell.
DESTINATION_LIKELY_EXCHANGE ask This looks like an exchange deposit address. Which exchange gave it to you?
TOKEN_UNREADABLE warn We couldn't read {symbol}, so this send wasn't fully checked.
NO_ISSUES ok No problems found.

New codes may be added. If you get one that isn't listed here, show message and act on the level.

Delisting notices

delistings() returns every active notice: { exchange, symbol, mint, kind, effectiveAt, announcedAt, sourceUrl }.

  • kind is delist, deposit-suspend or withdraw-deadline.
  • Every notice is pinned to an exact Solana mint. We never publish a notice by ticker alone, because tickers collide.
  • matchDelistings(notices, heldMints) keeps the ones your user holds. Run it on the device.
  • Link sourceUrl, the exchange's own announcement, but don't prefetch it.

Sources today: Binance, OKX, Bybit, KuCoin and Bitget delisting announcements, polled hourly.

Errors

HTTP Client behaviour Why
200 the response
400 throws GuardianError (bad_address, bad_mint, bad_user_mark, bad_body, not_a_mint) A bug on the calling side.
401 throws GuardianError (unauthorized) A missing or wrong key. It must never fail silently.
429, 5xx, timeout, network returns { level: 'unavailable', error } Fail open. Your UX decides, and we recommend "couldn't check".

The default timeout is 4 s. The service answers within about 3 s even for a never-seen address. createGuardian throws straight away on a missing key, a baseUrl that isn't an http(s) URL, or a runtime with no fetch. Caught later, those would read as "unavailable" on every call.

Coverage

Exchange Can block Notes
Binance, KuCoin, Bitget, HTX, Gate yes The exchange publishes the exact mint for each Solana deposit.
Coinbase yes, except tokens it lists without an address Those soften to warn.
Kraken no (warn) Its table is maintained by hand.
OKX, Bybit, MEXC no (CEX_NO_DATA warn) We recognise their addresses, but they publish no keyless listing data.
  • A deposit address that has been used before is usually recognised. A brand-new one answers ask.
  • Listing data refreshes weekly; GET /v1/health shows when (dataAsOf).
  • Warn, never guarantee. The guardian reduces lost deposits; it cannot promise an exchange's crediting.

Exchange ids

binance coinbase kraken kucoin gate bitget htx okx bybit mexc

Privacy

  • For each check we receive the recipient address, the mint, and optionally a symbol and the user's mark. Never the sender, the amount or the user's IP (your backend makes the call).
  • We keep exchange-address findings (an address that sweeps into Binance is Binance's, for every partner), token metadata, and per-key counts of verdicts. We keep no request logs with addresses.
  • Delisting matching runs on the device; holdings never reach us.