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

Your agent is…CallCost
considering a token and has no trade yet/analyse-tokenfree
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.

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

POST /v1/agent/analyse-token FREE

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.

FieldTypeNotes
mintstringRequired. 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.

POST /v1/agent/analyse $0.01

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.

FieldTypeNotes
inputMintstringRequired. Base58 pubkey.
outputMintstringRequired. Must differ from inputMint.
amountstringRequired. Atomic units as a decimal string — 1 SOL is "1000000000".
slippageBpsintegerOptional. 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.

POST /v1/agent/optimize $0.02

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.

FieldTypeNotes
takerstringRequired. 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.
analysisIdstringOptional. 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.

POST /v1/agent/bundle FREE

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.

FieldTypeNotes
signedTransactionstringRequired. 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.

SituationTypicalUpper bound
First scan of a token (not cached)1–5 s~12 s
Same mint again within 60 s, by anyoneunder 1 sunder 1 s
/optimize with a valid analysisId1–2 stoken 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.

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.

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.

ResponseerrorReason in PAYMENT-RESPONSEChargedWhat to do
200—YesThe result is in the body.
402settlement_failed_on_chain, settlement_not_landedNoThe transfer did not move. Sign a fresh authorization.
402settlement_pendingMaybeNot 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.

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.

StatusCodeChargedMeaning
400invalid_requestNoA field failed validation; the message names it.
400analysis_mint_mismatchNo/optimize: the analysisId is for another mint. Screen the right mint, or omit the ID.
402—NoPayment required. Not an error.
409authorization_already_usedNoThis 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.
409already_served, redemption_*, settlement_unresolvedAlready, or unknownA retry of a settlement_pending payment. charged says whether it was charged; see retry once. Nothing new is charged.
422not_a_mintNoAny 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.
422taker_insufficient_balanceNo/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.
429rate_limitedNoBack off Retry-After seconds; the authorization is not consumed.
502analysis_unavailableNoAn upstream failure left no honest answer. A slow source never causes this.
502optimize_unavailableNoNo transaction could be built or verified; causes says why.
500internal_errorNoUnexpected server error.