Rektopsy
Scam checkDocs
On this page
Getting started

Introduction

Rektopsy explains why a Solana transaction failed. Send it a transaction signature and it returns the cause in plain language, the exact fix, whether your funds are safe, and the evidence behind every claim: a log line, an instruction, or a balance field.

It also checks pasted messages for scams before they cost you anything: send it a DM, tweet, email or airdropped token name and it returns the red flags, quoted from the text. See Scam check.

Read-only by design. Rektopsy never connects to a wallet, never signs, and needs no keys. It only reads public transaction data.
Base URL
https://rektopsy-api.onrender.com
Authentication
None
Format
JSON over HTTPS

Quickstart

One request is all it takes. Send a failed signature, get an autopsy back.

curl
curl -X POST https://rektopsy-api.onrender.com/autopsy \
  -H "Content-Type: application/json" \
  -d '{"signature": "2KhUjNQsVikHC6JnpigNW4fhspGJYPM7SsgutEXjR6mj8ufLgSdVwA9KZEeMjCdhxGcMMMJG91rUyYM9h32c94zX"}'
JavaScript
const res = await fetch("https://rektopsy-api.onrender.com/autopsy", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ signature }),
});
if (!res.ok) throw new Error((await res.json()).detail);
const autopsy = await res.json();
console.log(autopsy.cause, autopsy.fix, autopsy.funds);
Python
import httpx

r = httpx.post("https://rektopsy-api.onrender.com/autopsy", json={"signature": signature}, timeout=90)
r.raise_for_status()
autopsy = r.json()
print(autopsy["cause"], autopsy["fix"], autopsy["funds"], sep="\n")

Or skip the API and use the web app, which renders the same response as a full report.

How it works

Every autopsy runs the same deterministic pipeline. Only the last step uses an LLM.

  1. 1
    Fetch
    Read the transaction from Solana mainnet with getTransaction (read-only, cached).
  2. 2
    Parse
    Walk the program log stack to find which program failed first, and with which numbers.
  3. 3
    Decode
    Name the error: program log, runtime error, known error table, or the program's on-chain Anchor IDL. Otherwise it is "unknown".
  4. 4
    Fix
    Pick the category and compute every fix value in Python, each from a documented rule.
  5. 5
    Explain
    An LLM turns those facts into three sentences: cause, fix, funds.
  6. 6
    Verify
    Reject the prose if it contains any number that is not in the facts. Retry once, then fall back to a template.
The core rule: the LLM never produces a number or a fact. It only phrases what Python extracted, and the verifier checks every number it writes. Without a Groq key the API still works, with template prose.
API reference

POST /autopsy

POSThttps://rektopsy-api.onrender.com/autopsy

Run an autopsy on one transaction. Requests take about 1 to 3 seconds.

Request body

fieldtypedescription
signaturestringRequired. The transaction signature: 86 to 88 base58 characters. Surrounding whitespace is ignored.
prosebooleanOptional, default true. false skips the LLM and returns template prose, faster and with 0 tokens; every fact is identical. Share cards use this.

Example response

Trimmed for length; … marks omitted fields.

200 OK
{
  "signature": "2KhUjNQs…h32c94zX",
  "category": "slippage",
  "cause": "The transaction failed because the Raydium AMM v4 exceeded the slippage limit you set.",
  "fix": "Retry with a fresh quote or lower the minimum you accept (your minimum was 5915877453 raw units, you would have received 5913883810 raw units, shortfall 1993643 raw units).",
  "funds": "Your money is safe; only the network fee of 0.000071794 SOL was charged.",
  "evidence": [
    { "claim": "cause", "source": "logs[6]" },
    { "claim": "fix values", "source": "logs[5] (ray_log)" },
    { "claim": "fix rule", "source": "minimum must be at most what the trade would give" },
    { "claim": "funds", "source": "meta.preBalances/postBalances, meta.preTokenBalances/postTokenBalances, meta.fee" }
  ],
  "log_lines": {
    "5": "Program log: ray_log: A/DuHF4JAAAATSCdYAEAAAABAAAA…",
    "6": "Program log: AMM error: Exceeded desired slippage limit."
  },
  "meta": { "mode": "llm", "attempts": 1, "tokens": 856, "rejected": [], "latency_ms": 2702 },
  "tx": {
    "slot": 451340359,
    "block_time": 1790604208,
    "failed_instruction": 1,
    "failure_log": 8,
    "steps": [
      { "index": 0, "label": "Compute budget set", "program_name": "Compute Budget", "status": "ok", … },
      { "index": 1, "label": "Raydium AMM v4 called", "program_name": "Raydium AMM v4", "status": "failed", … }
    ],
    "compute_consumed": 11763,
    "compute_limit": 19340,
    "programs_touched": 2,
    "logs": ["Program ComputeBudget111111111111111111111111111111 invoke [1]", …],
    "logs_truncated": false
  },
  "brief": {
    "category": "slippage",
    "cause": { "error_name": "ExceededSlippage", "custom_code": 30, "failing_program_name": "Raydium AMM v4", "resolved_by": "program log", … },
    "fix": {
      "action": "retry with a fresh quote, or lower the minimum you accept",
      "values": { "would_receive_raw": 5913883810, "your_minimum_raw": 5915877453, "shortfall_raw": 1993643, "shortfall_pct_of_minimum": "0.03" },
      "rule": "minimum must be at most what the trade would give",
      "simulated": false,
      …
    },
    "funds": { "safe": true, "fee_lamports": 71794, "fee_sol": "0.000071794", … }
  }
}

Response object

Top-level fields

fieldtypedescription
signaturestringThe signature you sent.
categorystringWhat kind of failure this is. See Categories.
causestringPlain-language cause of death.
fixstringWhat to change, with the exact value when the chain shows it.
fundsstringWhether funds moved, and the fee that was charged.
evidenceobject[]{ claim, source } for each claim. source is a log line (logs[i]), a transaction field, or the rule a value came from.
log_linesobjectThe text of every log line the evidence cites, keyed by index.
txobject | nullTransaction details for reports. null if it never reached the chain.
briefobjectThe structured facts the prose was written from. Use this, not the prose, for anything programmatic.
metaobjectHow the prose was produced: mode, attempts, tokens, rejected, latency_ms.

tx

fieldtypedescription
slot, block_timenumberWhere and when it landed. block_time is Unix seconds.
failed_instructionnumber | null0-based index of the top-level instruction that failed.
failure_lognumber | nullIndex into logs of the line where it failed.
stepsobject[]One per top-level instruction: index, label, program, program_name, status (ok, failed, not_run).
compute_consumed, compute_limitnumber | nullCompute units used, and the limit if the transaction set one.
programs_touchednumberDistinct programs invoked, including inner calls.
logs, logs_truncatedstring[], booleanThe full program log, and whether the RPC cut it short.

brief

fieldtypedescription
causeobjecterror_name, error_message, custom_code, failing_program, failing_program_name, called_by_program_name, account, resolved_by, source.
fixobjectaction, values (every number in the fix), rule (how they were computed), sources, simulated (always false for now).
fundsobjectsafe, fee_lamports, fee_sol, summary, source.

meta

fieldtypedescription
mode"llm" | "template"llm when the LLM's prose passed the number check; template otherwise.
attempts, tokensnumberLLM calls made (0 to 2) and total tokens used.
rejectedobject[]Drafts the verifier refused: { reason, numbers, text }. Empty when the first draft passed.
latency_msnumberServer-side time for the whole autopsy.

Categories

categorymeaningfix
slippageThe price moved past the tolerance you set.Jupiter: suggested slippage = min(max(2 × current, 0.5%), 3%). Others: what you'd have received vs your minimum.
compute_budgetIt ran out of compute units.New limit = consumed × 1.5, rounded up to 1,000, capped at 1,400,000.
insufficient_fundsNot enough SOL or tokens.The exact shortfall, in lamports and SOL, or raw and UI token units.
account_not_foundAn account it needed doesn't exist yet.The account's name, from the program's own error log.
unknown_customA custom error we can't identify.No guess: the error code and program to ask the app about.
never_landedgetTransaction returned nothing.Resend with a fresh blockhash. No funds moved, no fee charged.
succeededIt didn't fail.None.
otherA known error with no specific fix yet.General guidance only.

Fix values are suggested, not simulated: nothing is replayed against the chain.

Errors

Errors return JSON with a detail field.

statuswhendetail
400The signature isn't 86 to 88 base58 characters.A plain-language message.
422The body has no signature field, or isn't JSON.FastAPI validation errors (an array).
502Solana RPC failed or rate-limited the request.What went wrong. Retry after a few seconds.
400 (/scam-check)The text is empty or longer than 5,000 characters.A plain-language message.

A transaction that never landed is not an error: it returns 200 with category never_landed.

Other endpoints

GEThttps://rektopsy-api.onrender.com/health

Returns { "ok": true } when the API is up.

GEThttps://rektopsy-api.onrender.com/widget.js

The embeddable widget script. Cached for five minutes.

GEThttps://rektopsy-api.onrender.com/docs

Interactive OpenAPI explorer, generated by FastAPI: https://rektopsy-api.onrender.com/docs.

Scam check

POST /scam-check

POSThttps://rektopsy-api.onrender.com/scam-check

Check a pasted message for scam red flags: a DM, tweet, email, Telegram message, link, or the name of a token or NFT that showed up in a wallet. Requests take well under a second. Nothing you send is stored. The same check runs on the scam check page.

No LLM here. The verdict, every red flag and every piece of advice come from Python: fixed rules, public phishing lists and a small trained model. The verdict is never "safe": the best one is "no red flags found".

Request body

fieldtypedescription
textstringRequired. The message to check, 1 to 5,000 characters after trimming whitespace.
curl
curl -X POST https://rektopsy-api.onrender.com/scam-check \
  -H "Content-Type: application/json" \
  -d '{"text": "Hi, I am from Phantom support. Please share your recovery phrase at phant0m-app.io to restore your wallet."}'

Example response

A real response, trimmed; … marks omitted items.

200 OK
{
  "verdict": "likely_scam",
  "headline": "This looks like a scam.",
  "advice": [
    "Don't open the link. Type the real address yourself: phantom.app.",
    "Never share your seed phrase or private key. No real team, app or wallet will ever ask for it.",
    …
    "If you already signed something or shared your seed phrase, move your funds to a new wallet right away."
  ],
  "red_flags": [
    {
      "rule": "secret_request",
      "severity": "high",
      "title": "Asks for your secret phrase or private key",
      "why": "Anyone with these words owns your wallet. No real support team, app or wallet ever asks for them.",
      "match": "share your recovery phrase",
      "quote": "Please share your recovery phrase at phant0m-app.io to restore your wallet.",
      "start": 38,
      "end": 64
    },
    {
      "rule": "lookalike_link",
      "severity": "high",
      "title": "Link imitates a real site",
      "why": "In phant0m-app.io, the name \"phantom\" is used on a different site. The real site is phantom.app.",
      "match": "phant0m-app.io",
      "quote": "phant0m-app.io"
    },
    …
  ],
  "links": [
    {
      "host": "phant0m-app.io",
      "status": "lookalike",
      "detail": "the name \"phantom\" is used on a different site",
      "real": "phantom.app"
    }
  ],
  "model": {
    "score_pct": 65,
    "threshold": 0.69,
    "threshold_pct": 69,
    "flagged": false,
    "top_words": ["your", "your wallet", "from", "recovery phrase", "restore"],
    "trained_on": 8903
  },
  "lists_checked": ["MetaMask's phishing list", "Phantom's blocklist", "ScamSniffer's database"],
  "lists_unavailable": []
}

Response object

fieldtypedescription
verdictstringlikely_scam if any serious red flag fired, suspicious if only warning signs did, otherwise no_red_flags.
headlinestringOne plain sentence for the verdict.
advicestring[]What to do, chosen from fixed hand-written lines by the flags that fired.
red_flagsobject[]Each flag: rule, severity (high or medium), title, why, match (the exact words), quote (the sentence around them). start and end are character offsets into the trimmed text, present for text rules only.
linksobject[]Each domain found: host, status (official, lookalike, blocklisted, shortener, unknown), detail, and real (the genuine domain) for look-alikes.
modelobjectThe trained model's second opinion: score_pct, threshold_pct, flagged, top_words (the words that pushed the score up), trained_on (messages).
lists_checkedstring[]The phishing lists the links were checked against.
lists_unavailablestring[]Lists that couldn't be loaded this time. Links are still checked against the rest.

start and end count characters the Python way. In JavaScript, find the match text instead: offsets drift after an emoji.

Red flags

Any high flag makes the verdict likely_scam. Medium flags alone make it suspicious.

ruleseverityfires on
secret_requesthighAsks for your seed phrase, recovery phrase or private key. Warnings like "never share your seed phrase" don't count.
send_to_get_morehighSend crypto to get more back, or "double your SOL".
guaranteed_returnshighGuaranteed profits, "30% daily", risk-free returns.
verify_wallethighVerify, sync, validate or restore your wallet.
wallet_to_claimhighConnect, approve or sign to claim, mint, migrate or keep something.
mixed_alphabetshighA word mixing English letters with Cyrillic or Greek look-alikes ("Rауdium").
lookalike_linkhighA domain imitating a real one: swapped characters (jup1ter.ag), one letter off, a brand name on another site, or punycode.
blocklisted_linkhighA domain on Phantom's, MetaMask's or ScamSniffer's phishing list.
airdrop_linkhighAirdrop, reward or claim wording plus a site that isn't official: how spam tokens and NFTs lure you.
impersonationmediumClaims to be support, staff, a moderator or the team.
unsolicited_helpmedium"DM me", offers to recover your funds, "we noticed your…".
fake_threatmediumYour wallet is compromised, locked, or about to be liquidated.
free_rewardmediumUnclaimed rewards, "you're eligible", free airdrops.
visit_to_claimmediumVisit the site to claim, mint or use a whitelist.
urgencymediumDeadlines and scarcity: "expires in 1 hour", "only 500 spots left".
shortened_linkmediumA link shortener such as bit.ly that hides the address.
modelmediumThe trained model scored the text at or above its threshold. It can make a verdict suspicious, never likely_scam.

Phishing lists are downloaded when the API starts and refreshed daily. A link on no list shows status unknown: that isn't proof it's safe.

Accuracy

test setscams flaggednormal flagged
General scams held out from training (Discord, SMS)143 / 16611 / 1,614
Real Solana scams, blind14 / 320 / 607
Real Solana scams, after rules added for the misses21 / 320 / 607

The blind row is the honest number: those 32 real messages were collected from news and security write-ups without looking at the rules. The last row was tuned on the same messages. Most remaining misses are posts from hacked celebrity and brand accounts, where the text looks normal and the scam is the hack itself. Sources and the full test set are in the README.

Widget

Embed the widget

Add Rektopsy to any site with one script tag. The widget lives in a shadow root, so your CSS can't break it and its CSS can't leak into your page. It inserts API data as text only, never as HTML.

HTML
<script src="https://rektopsy-api.onrender.com/widget.js" defer></script>

<!-- Input box: the reader pastes any signature -->
<rektopsy-autopsy></rektopsy-autopsy>

<!-- Pinned: runs as soon as the page loads -->
<rektopsy-autopsy
  signature="2KhUjNQsVikHC6JnpigNW4fhspGJYPM7SsgutEXjR6mj8ufLgSdVwA9KZEeMjCdhxGcMMMJG91rUyYM9h32c94zX"
  report="https://rektopsy.vercel.app">
</rektopsy-autopsy>

Try it live first: the widget playground runs the real widget against the API and writes this code for you.

Attributes

attributedefaultdescription
signaturenoneRun immediately for this signature. Without it, the widget shows an input box. Changing it reruns.
apithe script's originWhere to send requests. Only needed if you host widget.js somewhere else.
reportnoneYour Rektopsy web UI. Adds a "Full autopsy" link to /report?sig=…; otherwise it links to Solana Explorer.
themedarkOnly dark for now.
The widget's retry button is a disabled preview. Rektopsy is read-only, so users change the setting in their own wallet or app.

Rektopsy.attach()

Prefer code over markup? attach() creates the element, appends it, and returns it.

JavaScript
const el = Rektopsy.attach({
  sig: failedSignature,              // optional: omit to show the input box
  target: "#support-panel",          // element or selector; defaults to <body>
  report: "https://rektopsy.vercel.app",
  theme: "dark",
});

Events

All events bubble out of the shadow root, so you can listen on document.

eventdetailfires when
rektopsy:resultthe autopsy JSONAn autopsy finished.
rektopsy:error{ message }The signature was invalid or the API failed.
rektopsy:dismissnoneThe reader closed the widget.
JavaScript
document.addEventListener("rektopsy:result", (e) => {
  const autopsy = e.detail;          // the full POST /autopsy response
  analytics.track("tx_autopsy", { category: autopsy.category });
});
document.addEventListener("rektopsy:error", (e) => console.warn(e.detail.message));
document.addEventListener("rektopsy:dismiss", () => closeSupportPanel());
Operations

Self-hosting

Rektopsy is open source under the MIT license. You need uv (Python 3.11) for the API, and Node.js 20.9+ with pnpm for the web UI.

Terminal
git clone https://github.com/SachinSigdel/rektopsy.git && cd rektopsy
uv sync
cp .env.example .env              # optional: add GROQ_API_KEY
uv run uvicorn rektopsy.api:app --port 8000

# web UI, in another terminal
cd web && cp .env.example .env.local && pnpm install && pnpm dev
variabledefaultpurpose
SOLANA_RPC_URLapi.mainnet-beta.solana.comAny mainnet RPC. The public one is rate-limited.
GROQ_API_KEYnoneEnables LLM prose. Free at console.groq.com.
GROQ_MODELopenai/gpt-oss-120bGroq model override.
REKTOPSY_CORS_ORIGINS*Comma-separated origins allowed to call the API.
NEXT_PUBLIC_API_URLhttp://localhost:8000Web UI only: where it finds the API. Set before building.

Limits

  • Mainnet only. Transactions are read from Solana mainnet-beta.
  • The hosted API runs on a free plan: after 15 idle minutes, the first request can take up to a minute while it wakes.
  • Solana's public RPC rate-limits heavy use. The API backs off and retries, then returns 502.
  • Fixes are suggested, not simulated.
  • Token amounts are in raw units (the token's smallest unit) when the token's decimals aren't known.
  • Coverage matches the programs Rektopsy has been tested on. Other programs fall back to their on-chain Anchor IDL, or to unknown_custom.
  • The scam check reads text only. It can't tell that an account was hacked, and a link on no list isn't proof it's safe.