Live · free · no key

The VAUTH API.

Seven endpoints. Four agents, a scanner, and a pre-trade gate built for code rather than for reading. Point them at a contract and get back what it can do, whether its code was ever published, whether a real holder can actually move their tokens, and whether any of that changes later. No account, no API key, no rate limit to apply for.

Getting started

Every endpoint is a POST with a JSON body, and every response is JSON. There is no authentication, so the shortest possible integration is one curl.

# map a contract on Robinhood Chain
curl -X POST https://vauth.link/api/agent/vautheye \
  -H "Content-Type: application/json" \
  -d '{"address":"0x5ecaA60dCffe1Ef33Fc61e47AFe3dd7b73DF6bdc"}'
ItemValue
Base URLhttps://vauth.link/api
The older host vauth-signal.catvillsdeveloper.workers.dev still answers and will keep answering, so nothing you already built breaks.
AuthNone. These endpoints are read only and public.
CORSOpen to every origin, so browser front ends can call it directly.
Content typeapplication/json, request and response.
User agentSend a real one. Cloudflare bot protection sits in front of this API and rejects some default library agents, notably Python-urllib, with a 403 before your request ever reaches us.

If you get a 403 with an HTML body rather than JSON, that is the bot filter and not this API. Set a User-Agent header identifying your app and it will go through. Requests libraries that set their own agent, including curl, fetch, and Python requests, are fine as they are.

The pre-trade gate

POST/verdict

Agents can hold wallets and trade on their own now. What was missing is a check they can make before they act. This endpoint is that check, and it is the only one here written for a program rather than a person: one call, a stable answer to branch on, and reason codes to log.

curl -X POST https://vauth.link/api/verdict \
  -H "Content-Type: application/json" \
  -d '{"token":"0xdAC17F958D2ee523a2206206994597C13D831ec7","chain":"eth"}'

{
  "verdict": "pass",          // "pass" | "fail" | "unknown"
  "risk": "medium",
  "safe": true,              // true only when verdict is "pass"
  "reasons": [
    { "code": "PAUSABLE", "level": "medium",
      "text": "The owner can freeze transfers…" }
  ],
  "checksRun": ["capabilities", "transferability"],
  "checksSkipped": []
}

How to branch on it

verdictWhat it means
failSomething we watched go wrong. A real holder could not transfer, approvals are refused, trading is frozen right now, or there is no contract at the address. Do not trade.
passEvery check we could run came back clean. Read risk and reasons before deciding how much to commit.
unknownA load-bearing check could not run, listed in checksSkipped. Treat this the way a careful person would, by staying out and asking again later.

Reason codes

CodeLevelMeaning
TRANSFER_BLOCKEDhighA wallet holding the token could not move it. The shape of a honeypot.
APPROVE_BLOCKEDhighApproving a spender is refused, so routers and exchanges would fail.
CURRENTLY_PAUSEDhighThe contract reports it is paused right now.
NOT_A_CONTRACThighThere is no code at the address.
MINTABLEmediumThe owner can create new supply.
PAUSABLEmediumThe owner can freeze transfers.
UPGRADEABLEmediumThe running code can be replaced.
PROXY_UPGRADEABLEmediumUpgradeable proxy: the code can change without the address changing.
OWNER_ACTIVEmediumOwnership has not been renounced.

Why powers are medium and not failures. DAI can mint. USDT can pause. Grading those as failures would have this gate reject the two most liquid stablecoins on earth, and you would rightly throw it away. Bytecode cannot tell a governed mint from a rug. It can only tell you the power exists, which means using that token involves trusting whoever holds it. So we surface it and let you decide, and reserve failure for harm we actually observed.

Why unknown exists at all. A check that could not run is never counted as a check that passed. If a node refuses us mid-probe, that is our problem and not evidence about the token, so it becomes a gap rather than an accusation. This is the rule the whole API is built on: silence beats a confident wrong answer.

VAUTHEYE, the recon agent

POST/agent/vautheye

Maps any address on Robinhood Chain. This is our own analysis rather than a call to somebody else's scanner, because no security vendor covers this chain. It reads the code from the chain, follows a proxy to the contract that actually executes, and lists every function that code exposes.

Request

FieldTypeNotes
addressstringRequired. A 0x address. Wallets are accepted and reported as such.
chainstringOne of robin, eth, base, bnb. Defaults to robin.

Reading bytecode does not care which chain it is on, so VAUTHEYE and VAUTHRON run on all four. Proxy resolution covers EIP-1167 clones, EIP-1967 slots, and the older OpenZeppelin slot that USDC and many contracts of that era still use.

Response

{
  "address": "0x5ecaA60dCffe1Ef33Fc61e47AFe3dd7b73DF6bdc",
  "kind": "contract",          // "contract" or "eoa"
  "codeBytes": 45,             // code at the address itself
  "logicBytes": 13804,         // code that actually runs
  "proxy": {
    "type": "EIP-1167 minimal proxy",
    "implementation": "0x581f7b996e6d3e436c537989157c9cb36421419b"
  },
  "token": {
    "name": "VAUTH by Virtuals",
    "symbol": "VAUTH",
    "decimals": "18",
    "totalSupply": "1000000000000000000000000000"
  },
  "owner": "0xe220329659d41b2a9f26e83816b424bdacf62567",
  "groups": {
    "erc20":  ["transfer(address,uint256)", "approve(address,uint256)"],
    "power":  ["owner()", "transferOwnership(address)"],
    "danger": [],
    "info":   ["burn(uint256)"]
  },
  "selectorCount": 91,        // functions found in the bytecode
  "unknown": 77,               // of those, ones we cannot name
  "verdict": "warn",           // "high" | "warn" | "ok" | "info"
  "headline": "Token contract mapped",
  "findings": [
    { "level": "warn", "text": "Ownership is live and held by 0xe220…2567…" }
  ],
  "steps": ["eth_getCode 0x5ecaA60d…", "decoded EIP-1167 clone target"],
  "explorer": "https://robinhoodchain.blockscout.com/address/0x5eca…",
  "tookMs": 2272
}

Fields worth understanding

FieldWhat it means
proxyNull when the address runs its own code. When present, every other reading refers to the implementation, because that is the code that executes.
selectorCountHow many function selectors were found by walking the bytecode. This works even when the source was never published.
unknownSelectors we will not put a name to. Unnamed is not the same as unsafe, it means the source is needed to read them. We report the count rather than guessing.
groups.dangerCapabilities that can hurt a holder: mint, pause, upgrade. An empty array means none were found in the code that runs.
tokenNull when the contract exposes a token interface but holds no values, which is the shape of a logic contract behind a proxy.
cachedAgeSecPresent when the answer came from cache. Seconds since it was read from the chain.
stalePresent and true when the chain was unreachable and we served the last complete reading instead.

Finding levels

  • high, a capability that can directly harm a holder
  • warn, worth understanding before you trust it, including anything we could not read
  • good, a property that reduces risk, only ever stated when it was actually verified
  • info, context with no risk attached

VAUTHOR, the provenance auditor

POST/agent/vauthor

VAUTHEYE answers what a contract can do. VAUTHOR answers the question underneath it: is the code you are trusting the code somebody actually published, and who put it there.

The case this agent exists for is the proxy. On a proxy the address everyone quotes holds no source of its own, so a verification check pointed at it reports fully published contracts as unverified. Our own token is one of those. VAUTHOR follows the proxy first, every time, and checks the contract that actually runs.

FieldTypeNotes
addressstringRequired. A 0x address on Robinhood Chain.
explorerobjectOptional, and recommended from a browser. {outer, logic}, the two /addresses/ records fetched from your own connection. See the rate limit note below.
{
  "verdict": "ok",
  "headline": "Provenance traced",
  "logicAddress": "0x581f7b996e6d3e436c537989157c9cb36421419b",
  "source": {
    "verified": true,
    "name": "AgentTokenV4",
    "compiler": "v0.8.26+commit.8a97fa7a",
    "verifiedAt": "2026-07-01T23:08:09Z"
  },
  "deploy": {
    "creator": "0x43E4C17b15365596Caae8e7d00E42Bc8E988c2d4",
    "creationTx": "0x61bef3c2…"
  },
  "findings": [ … ]
}

Blockscout rate limits by IP and answers a Cloudflare Worker with 429 Too many requests under any real load, so server side calls to it are unreliable. Browsers are not affected. Fetch /addresses/<address> and /addresses/<implementation> yourself and pass them as explorer. Responses built from supplied records are tagged via: "client" and are never cached.

Source match

POST/match

Verification proves the source on an explorer compiles to the deployed bytecode. It says nothing about whether the repository everybody is reading is that same source. This closes that gap: is the code they published the code they deployed?

It also catches a swapped dependency. A contract usually bundles its libraries into its verified source, so comparing those against the library's own repository shows whether the version that shipped is the version the library actually publishes. We ran it on our own token: all fourteen OpenZeppelin files inside the deployed contract are byte for byte identical to OpenZeppelin's published code.

Request

File bodies are far too large to send to an API, so both sides are hashed by the caller after the same normalisation, and we do the matching. Normalise by converting line endings to \n, trimming trailing whitespace on every line, dropping leading and trailing blank lines, then take a SHA-256 of the result.

FieldTypeNotes
chainFilesarrayRequired. [{path, sha}] from the verified source on the explorer.
repoFilesarrayRequired. [{path, sha}] from the repository.
address, repostringOptional labels, echoed back in the response.

Paths do not have to line up. @openzeppelin/contracts/utils/Address.sol on chain and contracts/utils/Address.sol in a repo are matched on file name, and when a name appears more than once the candidate sharing the longest path tail wins.

Response

verdictWhat it means
matchEvery deployed file has an identical counterpart in the repository.
partialSome match. differing and onlyOnChain name the rest.
mismatchNothing matched. The repository is not the source of this deployment.
unknownNo published source was supplied, so there was nothing to compare.

Identical text proves the two copies agree. It does not prove the repository is where the deployment came from, and a difference is not automatically sinister: reformatting, a different library version, or a flattened build all show up here. Read what differs before drawing a conclusion.

VAUTHRON, the adversarial probe

POST/agent/vauthron

The other agents read. This one pokes. Every probe is an eth_call, which executes the contract against live chain state and then throws the result away. No transaction, no gas, nothing signed, nothing moved.

The probe that matters: VAUTHRON finds a wallet that genuinely holds the token, then simulates a transfer out of it. If that reverts while the balance is there, holders cannot move their tokens, and that is the shape of a honeypot. Simulating from an empty wallet proves nothing, which is why it goes and finds a real holder first.

FieldWhat it means
probesEach probe with name, passed, and a short detail.
holderTestedThe wallet the transfer was simulated from. Null means no holder could be confirmed and the probe did not run, which is a gap in the test rather than a pass.
chainSame four chains as VAUTHEYE. On Robinhood Chain the holder list comes from the explorer; elsewhere pass explorer.holders from a GoPlus record you fetched.

Two things this agent refuses to get wrong. A supplied holder list is treated as a hint and never as evidence: the balance is confirmed on-chain before probing, so feeding in an empty wallet cannot manufacture a "holders cannot transfer" verdict against a healthy token.

And an empty return value is not a failure. ERC-20 predates the convention that transfer returns a bool, so USDT and much of its era return nothing at all. Reading that as a revert would have this agent calling USDT a honeypot.

VAUTHEON, the sentinel

POST/agent/vautheon

The other three answer once. This one keeps asking. It records a fingerprint of a contract, the owner, the address a proxy delegates to, and the size of the running code, then re-takes it every thirty minutes.

Those three fields are how a contract quietly becomes a different contract while its address never changes. Subscribe through the Telegram bot with /watch 0x… and you get a message the moment one of them moves.

FieldWhat it means
fingerprint{owner, impl, bytes} as measured right now.
watchersHow many Telegram subscribers are on alert for this address.
eventsChanges recorded since the watch began, newest first.

A read that fails comes back null, and null is not a change, it is a missing measurement. VAUTHEON will not raise an alert on one, and will not overwrite a known value with it, because a sentinel that cries wolf over a flaky RPC gets muted and then it is worth nothing.

Contract scanner

POST/scan

A token safety check across four chains. On Ethereum, BNB Chain, and Base it runs against GoPlus Security for honeypot behaviour, buy and sell tax, and owner privileges. On Robinhood Chain, which no honeypot scanner reaches, it reports only what the explorer can prove.

Request

FieldTypeNotes
addressstringRequired. The token contract address.
chainstringOne of eth, bnb, base, robin. Defaults to eth.
goplusobjectOptional. A GoPlus token record you fetched yourself, for us to grade. See the note below on why you may want this.

Read this before you build on the eth, bnb, or base chains. GoPlus throttles by IP, and this API runs on Cloudflare, whose egress addresses are shared with a great deal of other traffic. In practice that means our server side calls to GoPlus are often refused with an upstream code of 4029, and you get a 503 saying so.

Browsers are not affected, because the request comes from the reader's own connection. So the pattern we use on our own scanner page, and the one we recommend to you, is to fetch the GoPlus record yourself and pass it as goplus. You keep the reliability of your own IP, and we still do the grading, so the verdict logic stays in one place.

Results built from a supplied record are never cached, since a caller could otherwise shape what other people see. Robinhood Chain has no such problem and works server side.

// browser pattern for eth, bnb, and base
const gp = await fetch(
  `https://api.gopluslabs.io/api/v1/token_security/1?contract_addresses=${addr.toLowerCase()}`
).then(r => r.json());

const report = await fetch('https://vauth.link/api/scan', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    address: addr,
    chain: 'eth',
    goplus: gp.result[addr.toLowerCase()]
  })
}).then(r => r.json());

Response

{
  "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
  "chain": "eth",
  "chainName": "Ethereum",
  "mode": "full",               // "full" or "basic" on Robinhood Chain
  "verdict": "warn",
  "name": "USD Coin",
  "symbol": "USDC",
  "holders": "8062846",
  "high": [],
  "warn": ["Proxy contract, logic can be swapped"],
  "good": ["Source code verified", "Ownership renounced"],
  "explorer": "https://etherscan.io/address/0xA0b8…",
  "note": "Automated check, not a guarantee…"
}

Errors

Failures return a JSON body with an error string you can show to a user as written.

StatusWhen
400The address is not a 0x address, the chain is unsupported, or the body is not valid JSON.
404The scanner genuinely has no record of that address on that chain. It may be too new, or not a token.
502The chain could not be reached and there was no cached answer to fall back on.
503The upstream safety scanner refused us, usually GoPlus throttling. The error text names the reason. This says nothing about the token.

A partial reading is never dressed up as a clean one. If a lookup fails, you get either an explicit error, or a report whose findings say what could not be read. The API will not tell you a contract looks fine when it simply could not see it.

Limits, caching, and fair use

  • Answers are cached for about 15 minutes per address. Repeat calls are fast and carry cachedAgeSec.
  • If the chain is unreachable, a complete older reading is served with stale: true rather than a broken fresh one.
  • There is no published rate limit. It runs on free public infrastructure, so please cache on your side and avoid tight loops.
  • If you are building something that needs volume, get in touch on X and we will work out what you need.

What this cannot do

An automated reading is not an audit. Bytecode tells you what a contract is capable of, not what its operators intend. A contract can pass every check here and still be run by someone who plans to rug it, and 77 unnamed functions is exactly that many things we did not read for you.

Treat a clean result as one input among several, never as permission. That is how we use it ourselves, including on our own token, which this API will tell you has ownership that is not renounced.