Swap protection,
priced per call.
A pre-signing safety check and swap builder for agents that trade on Solana. Before your agent trades, ZendIQ answers two questions: is this token safe to hold, and how should this swap be executed? Screening a token is free; a verdict costs $0.01 and a built transaction $0.02, paid per call in USDC over x402. No API key, no account, no custody.
Why, when, and how to start
- It answers before you sign. Token risk (16 rug and honeypot checks) and sandwich exposure are scored at the moment of the trade, from live mainnet data.
- Every claim can be checked. A verdict comes with its reasons and the checks behind it; a built transaction comes with the plan its bytes should match and every fee itemised. An unanswered check is reported as
unknown, never as a pass. - It compares routes for you.
/optimizeprices Jupiter, Raydium and Jito bundles after fees, tip and modelled sandwich loss, and shows the arithmetic. Often the answer is Jupiter.
| Your agent is… | Call | Cost |
|---|---|---|
| considering a token and has no trade yet | /analyse-token | free |
| about to swap and deciding whether, and how, to trade | /analyse | $0.01 |
| decided to trade and needs the transaction | /optimize | $0.02 |
Quickstart
1. Screen a token, free. No wallet, no payment:
curl -s -X POST https://api.zendiq.ai/v1/agent/analyse-token \
-H "content-type: application/json" \
-d '{"mint":"DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263"}'
PowerShell:
Invoke-RestMethod -Method Post https://api.zendiq.ai/v1/agent/analyse-token `
-ContentType 'application/json' `
-Body '{"mint":"DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263"}' | Select-Object -ExpandProperty tokenRisk
Python, standard library only:
import json, urllib.request
req = urllib.request.Request(
"https://api.zendiq.ai/v1/agent/analyse-token",
data=json.dumps({"mint": "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263"}).encode(),
headers={"content-type": "application/json"},
)
with urllib.request.urlopen(req, timeout=30) as resp:
body = json.load(resp)
risk = body["tokenRisk"]
print(risk["symbol"], risk["score"], risk["level"], "signals", body["signals_resolved"])
You get tokenRisk (score and level), the signals behind it with coverage in
signals_resolved, and an analysisId valid for 60 seconds.
2. Get a verdict, $0.01 in mainnet USDC. POST { inputMint, outputMint, amount } to
/v1/agent/analyse. The first reply is 402 with the price; sign the USDC
authorization it describes from a wallet holding mainnet USDC (no SOL needed) and resend the same request with a PAYMENT-SIGNATURE header
(paying for a call). Any x402 client does this for you, as does
npm run analyse in the reference repository.
Read verdict and reasons.
3. Build the swap, $0.02 in mainnet USDC. POST the same fields plus taker (the wallet that
will sign) and the analysisId from step 1 to /v1/agent/optimize. Check
transaction against plan, sign it with the taker, and submit it the way
submit.method says.
For machines
Everything below is also published in machine-readable form. The live manifest is the authoritative source — fetch it at runtime rather than hardcoding what you read on this page.
GET /v1/agent— live service manifest. Free, unauthenticated. Declares every endpoint, price, rate limit, and the field-stability contract./openapi.json— OpenAPI 3.1 description of all four endpoints./llms.txt— site index in llmstxt.org format./llms-full.txt— the complete API reference in one file.- Reference client + MCP server — runnable examples and a Model Context Protocol server exposing three tools:
zendiq_screen_token(free),zendiq_triage_swap($0.01) andzendiq_optimize_swap($0.02). Install it in Claude Code withclaude mcp add --scope user zendiq -- npx -y @zendiq/mcp@latest, or runnpx -y @zendiq/mcp@latestfrom any MCP client's user config (a project.mcp.jsonasks for approval first); the free tool needs no wallet, and the paid tools need a spend ceiling (npx -y @zendiq/mcp@latest budget init 1.00) and a paying key in~/.zendiq. The MCP server runs locally over stdio; agents that discover us from this page can call the endpoints directly over HTTP with x402, with nothing to install.
Weekly build videos — a dated build log from the Colosseum Crypto World's Fair. Each video states what ran locally and what ran live.
Endpoints
Base URL: https://api.zendiq.ai
Screens a single SPL token mint and returns a risk score with the signals behind it. Registered ahead of the payment gate, so it is never charged. Rate-limited.
| Field | Type | Notes |
|---|---|---|
mint | string | Required. Base58 pubkey. |
If the token cannot be scored, the call returns 502 analysis_unavailable rather than a
score it could not compute; there is no degraded 200 on this endpoint. A screen that could
not run never looks like a clean screen.
A slow source is not a failure: it comes back as an unknown check inside a scored
200. The response carries an analysisId that /optimize can reuse for 60 seconds.
Returns a Safe / Protect / Refuse verdict for a
proposed swap, with token risk, sandwich exposure, and a recommended execution path.
Builds no transaction. Always settles on success — a Refuse verdict is a
useful answer, not a failure.
| Field | Type | Notes |
|---|---|---|
inputMint | string | Required. Base58 pubkey. |
outputMint | string | Required. Must differ from inputMint. |
amount | string | Required. Atomic units as a decimal string — 1 SOL is "1000000000". |
slippageBps | integer | Optional. 0–10000. |
curl -X POST https://api.zendiq.ai/v1/agent/analyse \
-H "Content-Type: application/json" \
-d '{
"inputMint": "So11111111111111111111111111111111111111112",
"outputMint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"amount": "1000000000",
"slippageBps": 50
}'
verdict is a closed enum within v1 — no new values will be added
inside this version, so you can switch on it exhaustively.
Takes everything /analyse takes plus a required taker, and returns
an unsigned swap transaction, the structured plan it is
supposed to contain, how to submit it, a simulation result, an itemised
netBenefit breakdown, and the same verdict, confidence
and reasons as /analyse. A failed build charges nothing.
Reading netBenefit.
netUsd = expectedMevLossUsd − zendiqFeeUsd − jitoTipUsd − jupiterPlatformFeeUsd − priorityFeeUsd:
the sandwich loss the route avoids, less every fee you pay to execute it. It is stated
only on routes that claim MEV protection (jupiter_ultra and the Jito bundle venues) and is
null by design elsewhere. jupiterPlatformFeeUsd is Jupiter's own fee on an
Ultra trade, 0–50 bps by pair (2 bps on SOL–USDC); it is already inside the quoted amounts,
so the venue comparison is unaffected, and it is 0 on every other venue.
priorityFeeUsd is decoded from the transaction your taker signs: 0 on a bundle
or a gasless fill. The 5,000-lamport base signature fee is the same on every route and is not included.
A cost that cannot be priced is unknown, never zero: netUsd is
then null and netUsdBasis says why.
| Field | Type | Notes |
|---|---|---|
taker | string | Required. The mainnet wallet that will sign and submit. Must hold the input amount and SOL for fees and rent, except on a gasless Ultra fill. Can be the paying wallet or a different one. |
analysisId | string | Optional. From /analyse-token or /analyse for this outputMint, within 60 s. Skips the token scan. See reusing an analysis. |
Paying wallet and taker. The paying wallet signs the x402
payment: it holds mainnet USDC and needs no SOL, because the facilitator sponsors the payment's
network fee. The taker is the mainnet wallet the swap is built for, and the one that
signs it. One wallet can play both roles, or you can keep them apart. The taker
must hold the input amount and mainnet SOL for the network fee, any priority fee, and
rent for each token account the swap opens; on a SOL input, the SOL being swapped comes on top.
A gasless Jupiter Ultra fill is exempt from the SOL, because Jupiter is the fee payer
(plan.priorityFee.feePayer is not your taker). A taker that
cannot fund the trade gets 422 taker_insufficient_balance, uncharged, naming the token
and the shortfall.
Build-only trial. taker is only a public key, and nothing is signed
here, so you can pass any funded mainnet address (one holding the input amount and some SOL) to see
the full plan, simulation and plan.venueDecision. This is
build-only: you cannot sign or submit a transaction built for a wallet you do not control. To land
a trade, pass your own wallet.
The venue is chosen by risk, then by net benefit. Read plan.venue rather than
assuming one. jupiter_ultra is the default, and stays the baseline whenever the verdict
calls for protection, because its upstream MEV protection addresses sandwich exposure; Jupiter
sizes the fee and it cannot be overridden. jupiter_swap (Quote + Build) is used
when risk scoring calls for a specific priority fee. Direct venues are quoted alongside and win
only when they beat the Jupiter route after every cost: priority fee, Jito tip, modelled sandwich
loss, and for a bundle its landing risk (an assumed 5% chance you pay $0.02 to rebuild). The margin
to beat is 0.1% of the trade, capped at $1: enough that quote noise cannot flip the
venue, small enough that a real saving still wins, and capped because on a large trade a few
dollars is a genuine price difference.
Unprotected raydium competes only on a Safe verdict. The Jito bundle venues
(raydium_jito, jupiter_swap_jito) compete on every verdict, since a
bundle is never less protected than the Jupiter route. On Safe they step aside while ZendIQ's
shared Jito budget (about one bundle a second) is more than half used, so trades that need
protection always have it. plan.venueDecision shows every candidate's arithmetic,
and often the answer is Jupiter. On every venue, plan.priorityFee.appliedLamports
is decoded from the transaction bytes — the fee you are about to sign, not a requested figure.
Submit the way submit.method says.
jupiter_ultra_execute: sign transaction, then POST
{ signedTransaction, requestId } to submit.url. Broadcasting it yourself
forfeits Ultra's MEV protection. rpc_send_transaction: requestId is
null; sign and send to your own Solana RPC — the priority fee is already inside.
On raydium, send promptly: Raydium embeds its own blockhash and reports no expiry,
so submit.lastValidBlockHeight is null.
Read verdict and tokenRisk before signing.
/optimize returns the same verdict (Safe / Protect / Refuse),
confidence and reasons as /analyse, but does not refuse
to build: a Refuse still comes back with a transaction, because calling
/optimize means you have decided to trade. Do not sign a Refuse
unless you mean to trade against it. A tokenRisk of available: false
means the token was never screened.
Do not blind-sign. plan and netBenefit exist
precisely so you can verify the bytes before signing them: plan states the venue,
slippage and route the transaction should contain, and netBenefit itemises every
cost so the stated net can be checked rather than trusted. Decode the transaction and confirm
it matches before you sign.
simulation.status is ok, failed, or
unknown. An unknown simulation is not a passing simulation.
Submits a signed Jito bundle transaction returned by /optimize (when
submit.method is jito_bundle) and reports whether it landed. Free and
rate-limited: you already paid at /optimize. Only the exact transaction
/optimize issued in the last 5 minutes, carrying its Jito tip, is accepted.
Idempotent: resubmitting returns the first submission and never sends a second bundle.
GET /v1/agent/bundle/<signature> returns the status.
| Field | Type | Notes |
|---|---|---|
signedTransaction | string | Required. Base64, signed by the taker. |
Latency, unknowns and reusing an analysis
Most of a call's time is token screening: sixteen checks across on-chain data, RugCheck, DexScreener, GeckoTerminal and pump.fun. One scan per mint is shared by all callers and cached for 60 seconds.
| Situation | Typical | Upper bound |
|---|---|---|
| First scan of a token (not cached) | 1–5 s | ~12 s |
| Same mint again within 60 s, by anyone | under 1 s | under 1 s |
/optimize with a valid analysisId | 1–2 s | token scan skipped |
Measured on the live service on 2 October 2026 across USDC, BONK, JUP, RAY, POPCAT and six
pump.fun mints under a minute old: first scans took 0.5–4.6 s. The upper bound comes
from the per-source timeouts; the longest is the token's signature history, two pages of up to
6 s each. Creator history has its own 4 s budget.
Set your client timeout to at least 120 seconds on /analyse and
/optimize, because settlement can add up to 90 s.
On the free /analyse-token, 30 seconds is enough.
No partial results
A scan always runs to completion. It is never cut short and returned with checks missing, because an agent cannot act on a partly scanned token: a missing creator-history check looks exactly like a clean one to anything that does not read every field.
One read is bounded on purpose: creator history (creator_launches_30d) has a 4 s
budget. When a creator's record is longer, the launches counted so far come back as a lower
bound with reason: "partial_scan_lower_bound", and the status is warn
or fail, never pass.
Reading unknowns
Each source has its own timeout. One that errors or runs out its timeout is a real
unknown. The scan still completes and says what it could not find out.
- Every check is in
signals[]with astatusofpass,warn,failorunknown; anunknowncarries areason. signals_resolvedis the coverage, as resolved checks over the total:"11/16"means five are unknown.- An unknown never lowers the score. Most add an explicit
…: unavailablefactor with +5 caution points, so an unchecked token cannot score as clean. - A market figure no real token can have (a market cap above $5T, or a 24h gain above +10,000% on a token over 30 days old) is an
unknownwithreason: "implausible_value".valueis null and the rejected figure is inrejected_value; do not quote it as fact. - Market fields come from one DexScreener pool, named in
inputs.marketPairwith how many pools were considered and how many were dropped for disagreeing with the median price by more than 5×. schema_versionis per endpoint:/analyseand/analyse-tokenare at1.2,/optimizeat1.1.- The exception is a short list of established assets: regulated stablecoins, established protocol tokens such as SOL, JUP and RAY, and liquid staking tokens. An asset-class factor such as
Regulated stablecoinnames the checks that do not apply to that class; those are not scored, so an unknown on one of them adds nothing. Every other check still counts.
Treat unknown as unknown, never as safe. A LOW score at 6/16 means ten checks were never answered.
Reusing an analysis: analysisId
/analyse-token and /analyse return an analysisId. Pass it
to /optimize within 60 seconds and it reuses that token scan instead of running a
new one. Token facts are stable over seconds; market state is not. So the reused parts are the
authorities, holder concentration, RugCheck report, launch platform, token age, creator history
and the bundled-launch check. The quote, price impact, sandwich exposure and congestion are always
fetched fresh.
POST /v1/agent/analyse-token { "mint": "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263" }
→ { "analysisId": "an_Q2hsZ3v0mK7pX1aRt9Yc", "analysisExpiresAt": "…", "tokenRisk": { … }, … }
POST /v1/agent/optimize { "inputMint": "So111…112", "outputMint": "DezXAZ8z…B263",
"amount": "10000000", "taker": "<your wallet>",
"analysisId": "an_Q2hsZ3v0mK7pX1aRt9Yc" }
→ { "analysis": { "source": "reused", "reason": null, "ageSeconds": 4, … },
"transaction": "…", … }
A missing, expired or unknown ID is not an error. /optimize resolves
the scan itself and says so. analysis.source is full_scan, or
cached when a scan of that mint from the last 60 s was found. reason is
no_analysis_id, analysis_id_expired or analysis_id_unknown.
IDs live in server memory, so a restart makes earlier ones unknown: the next call is slower, not broken.
An ID for a different mint is rejected with 400 analysis_mint_mismatch,
uncharged, rather than silently replaced. Pairing one token's analysis with another token's trade
is a caller bug, and fixing it quietly would hide it while you believed you were trading on a
screened token. The ID must match /optimize's outputMint.
Paying for a call
ZendIQ uses the x402 protocol.
There are no API keys and no accounts. You POST your request, the server replies
402 with a challenge describing the price, asset, network and payee, you sign an
authorization for exactly that amount, and you retry the identical request with a
PAYMENT-SIGNATURE header. X-PAYMENT is accepted as an alias; a bare
PAYMENT header is not read and will return another 402.
- Requests are validated before the
402. A malformed body gets400 invalid_request, an address that is not a token mint gets422 not_a_mint, and on/optimizeatakerthat does not hold the input amount gets422 taker_insufficient_balance, so you never sign a payment for a request that cannot be served. - A
429does not consume a payment authorization. Back off forRetry-Afterseconds and present the same one again. - Any response
≥ 400cancels settlement. A failed/optimizebuild charges nothing. - Rate limits: 60 requests per IP per minute and 30 per payer per minute on the paid endpoints. The free
/analyse-tokenhas its own limit of 240 per IP per minute. The live figures are inrateLimitsatGET /v1/agent. DeprecationandSunsetheaders (RFC 8594) are set ahead of the payment gate, so you can detect a retiring version from the402without paying.
Settlement can hold the response for up to 90 seconds
The server settles your payment after your result is ready, and replies only once it knows whether the transfer landed. That usually takes a second or two. If the facilitator cannot confirm the transfer itself, the server reads the chain until the transfer confirms, fails, or can no longer land because the blockhash you signed against has expired, about 60 s after you signed. It never waits more than 90 s.
Keep the connection open: disconnecting does not stop a transfer that was already sent. If you
do disconnect, present the same authorization again. While it is still settling that returns
409 with priorOutcome: in_flight; once settled, the original
200 with replayed: true.
| Response | errorReason in PAYMENT-RESPONSE | Charged | What to do |
|---|---|---|---|
200 | — | Yes | The result is in the body. |
402 | settlement_failed_on_chain, settlement_not_landed | No | The transfer did not move. Sign a fresh authorization. |
402 | settlement_pending | Maybe | Not confirmed either way within 90 s. Keep the payment header and retry once with the same header after a few seconds. Do not sign a new authorization. |
Retry once after settlement_pending
The server keeps checking the chain after it answers. If the transfer lands, the payment is
recorded as charged and not served, and presenting the same
PAYMENT-SIGNATURE header again redeems it for one fresh response. The body may
differ, so an /optimize is rebuilt from current quotes. Wait about 5 s first.
- Once. The redeemed response carries
PAYMENT-RESPONSEwithredeemed: trueand the original settlementtransaction. A later retry gets409 already_served. - Same route. An
/analysepayment ($0.01) cannot be redeemed on/optimize($0.02):409 redemption_route_mismatch. - Within 24 h. After that,
409 redemption_expired; email support@zendiq.ai with the signature for a refund. - A failed build does not use it up. An error status leaves the payment redeemable.
- Still unknown.
409 settlement_unresolvedwithcharged: null. Do not pay again.
Each of these 409s carries charged and the settlement signature. The example client and the MCP server retry for you.
Custody
None, on any endpoint. /analyse and /analyse-token build nothing.
/optimize returns an unsigned transaction that only your own wallet can sign and
submit. ZendIQ never sees a key, cannot move funds, and there is no deposit or approval to
unwind.
When it cannot answer
ZendIQ returns 502 analysis_unavailable rather than a guessed verdict when an
upstream source fails and no honest answer is possible. You are not charged. Sub-analyses
degrade independently to explicit available: false objects rather than quietly
returning a safe-looking score they could not compute, and the top-level degraded
field names which ones — null when token screening, route and sandwich exposure
all produced a result, otherwise an array of "source:reason" strings. It does not
report individual token checks: a completed scan at 7/16 still has
degraded: null, and its unknowns are in signals[].
This is deliberate. A safety check that fails open is worse than no check at all, because you have already calibrated trust on it and get no signal that it did not run.
Errors
Only a 200 from a paid endpoint is charged. Every status ≥ 400 cancels settlement.
| Status | Code | Charged | Meaning |
|---|---|---|---|
400 | invalid_request | No | A field failed validation; the message names it. |
400 | analysis_mint_mismatch | No | /optimize: the analysisId is for another mint. Screen the right mint, or omit the ID. |
402 | — | No | Payment required. Not an error. |
409 | authorization_already_used | No | This payment authorization was already presented; read priorOutcome. For settle_threw or settle_unknown the transfer may have landed: check it on chain before paying again. |
409 | already_served, redemption_*, settlement_unresolved | Already, or unknown | A retry of a settlement_pending payment. charged says whether it was charged; see retry once. Nothing new is charged. |
422 | not_a_mint | No | Any endpoint: field is not a token mint. reason is account_not_found (usually a typo) or not_a_mint_account (a wallet or token account). No score is returned. Sent before any 402. |
422 | taker_insufficient_balance | No | /optimize: the taker lacks the input amount or the SOL for fees and rent. The body names the token, the amount required and held, and the shortfall. |
429 | rate_limited | No | Back off Retry-After seconds; the authorization is not consumed. |
502 | analysis_unavailable | No | An upstream failure left no honest answer. A slow source never causes this. |
502 | optimize_unavailable | No | No transaction could be built or verified; causes says why. |
500 | internal_error | No | Unexpected server error. |