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"}'
| Item | Value |
|---|---|
| Base URL | https://vauth.link/api The older host vauth-signal.catvillsdeveloper.workers.dev still answers and will keep answering, so nothing you already built breaks. |
| Auth | None. These endpoints are read only and public. |
| CORS | Open to every origin, so browser front ends can call it directly. |
| Content type | application/json, request and response. |
| User agent | Send 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
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
| verdict | What it means |
|---|---|
| fail | Something 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. |
| pass | Every check we could run came back clean. Read risk and reasons before deciding how much to commit. |
| unknown | A 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
| Code | Level | Meaning |
|---|---|---|
| TRANSFER_BLOCKED | high | A wallet holding the token could not move it. The shape of a honeypot. |
| APPROVE_BLOCKED | high | Approving a spender is refused, so routers and exchanges would fail. |
| CURRENTLY_PAUSED | high | The contract reports it is paused right now. |
| NOT_A_CONTRACT | high | There is no code at the address. |
| MINTABLE | medium | The owner can create new supply. |
| PAUSABLE | medium | The owner can freeze transfers. |
| UPGRADEABLE | medium | The running code can be replaced. |
| PROXY_UPGRADEABLE | medium | Upgradeable proxy: the code can change without the address changing. |
| OWNER_ACTIVE | medium | Ownership 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
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
| Field | Type | Notes |
|---|---|---|
| address | string | Required. A 0x address. Wallets are accepted and reported as such. |
| chain | string | One 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
| Field | What it means |
|---|---|
| proxy | Null when the address runs its own code. When present, every other reading refers to the implementation, because that is the code that executes. |
| selectorCount | How many function selectors were found by walking the bytecode. This works even when the source was never published. |
| unknown | Selectors 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.danger | Capabilities that can hurt a holder: mint, pause, upgrade. An empty array means none were found in the code that runs. |
| token | Null when the contract exposes a token interface but holds no values, which is the shape of a logic contract behind a proxy. |
| cachedAgeSec | Present when the answer came from cache. Seconds since it was read from the chain. |
| stale | Present 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
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.
| Field | Type | Notes |
|---|---|---|
| address | string | Required. A 0x address on Robinhood Chain. |
| explorer | object | Optional, 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
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.
| Field | Type | Notes |
|---|---|---|
| chainFiles | array | Required. [{path, sha}] from the verified source on the explorer. |
| repoFiles | array | Required. [{path, sha}] from the repository. |
| address, repo | string | Optional 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
| verdict | What it means |
|---|---|
| match | Every deployed file has an identical counterpart in the repository. |
| partial | Some match. differing and onlyOnChain name the rest. |
| mismatch | Nothing matched. The repository is not the source of this deployment. |
| unknown | No 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
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.
| Field | What it means |
|---|---|
| probes | Each probe with name, passed, and a short detail. |
| holderTested | The 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. |
| chain | Same 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
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.
| Field | What it means |
|---|---|
| fingerprint | {owner, impl, bytes} as measured right now. |
| watchers | How many Telegram subscribers are on alert for this address. |
| events | Changes 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
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
| Field | Type | Notes |
|---|---|---|
| address | string | Required. The token contract address. |
| chain | string | One of eth, bnb, base, robin. Defaults to eth. |
| goplus | object | Optional. 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.
| Status | When |
|---|---|
| 400 | The address is not a 0x address, the chain is unsupported, or the body is not valid JSON. |
| 404 | The scanner genuinely has no record of that address on that chain. It may be too new, or not a token. |
| 502 | The chain could not be reached and there was no cached answer to fall back on. |
| 503 | The 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: truerather 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.
