Appearance
Audit & safety API
Public, unauthenticated endpoints for the contract auditor and the safety suite. Mounted under /api/v1.
Product pages: contract auditor, safety suite.
Contract audit — /v1/audit
Route file: v1/routes/audit.route.js. Service: v1/services/contractAudit.service.js. Model: models/contractAuditModel.js.
| Method | Path | Returns |
|---|---|---|
GET | /v1/audit/report/:id | Audit JSON |
GET | /v1/audit/certificate/:id | Certificate JSON — only if certificateEligible, otherwise 404 |
GET | /v1/audit/usage?wallet= | { used, limit, remaining } |
Audit document
| Field | Meaning |
|---|---|
auditId | 16-char uuid slice — the public report id |
certificateId | 16-char uuid slice — the public certificate id |
wallet | Requesting wallet, used for quota |
score | Overall security score |
findings[] | Severity, title, description, location |
severityCounts | { critical, high, medium, low, informational } |
certificateEligible | critical === 0 && high === 0 |
status | success | failed |
Quota
js
await SystemSetting.setValue('audit_free_limit_per_wallet', 5) // default 3Failed audits do not consume quota
getAuditUsage counts with status: { $ne: 'failed' }. A user whose audit crashed because Etherscan timed out is not penalised.
Source acquisition
Etherscan v2 unified API — one key, all chains:
https://api.etherscan.io/v2/api?chainid={chainId}&module=contract&action=getsourcecode&address=…Supported: Ethereum, BSC, Polygon, Arbitrum, Optimism, Base, Linea, Avalanche, Fantom.
Multi-file contracts come back as a JSON blob and are flattened with // File: <path> headers.
Source is clamped to 40,000 characters
The truncation flag is stored on the audit document, so nobody mistakes a partial audit for a complete one. Large protocols get a partial pass.
Share URLs
{AUDIT_PUBLIC_BASE_URL}/report/{auditId}
{AUDIT_PUBLIC_BASE_URL}/certificate/{certificateId}AUDIT_PUBLIC_BASE_URL defaults to https://audit.blazpay.ai. The pages themselves are frontend routes outside MainLayout.
Token safety — /v1/safety
Route file: v1/routes/safety.route.js. Service: v1/services/tokenSafety.service.js. Model: models/tokenSafetyScanModel.js.
| Method | Path | Returns |
|---|---|---|
GET | /v1/safety/token/:address?chainId=… | Scan JSON |
GET | /v1/safety/scan/:id | Scan JSON by scanId |
GET | /v1/safety/token/:address/badge.svg?chainId=… | Embeddable SVG badge |
Route order
badge.svg is registered before /token/:address. Reversing them would make the badge path resolve as an address.
Scan output
| Field | Values |
|---|---|
score | 0–100 |
verdict | safe | caution | risky | dangerous |
findings[] | Human-readable issues |
checks[] | ownership_renounced, mintable, blacklist, tax, proxy, honeypot |
recommendations | What the user should do |
Pipeline
- Etherscan v2
getsourcecode(library files filtered out). - Etherscan v2
tokeninfo— symbol, decimals, supply, holders. - Etherscan v2
tokenholderlist— top 10 → concentration, burn and locker detection (PinkLock, Unicrypt, Team.Finance addresses hardcoded). - The safety model with a rug-focused prompt in JSON mode.
- Persist.
Caching
A scan less than 6 hours old for the same (address, chainId) is reused, skipping the model call.
This cache is what makes Autopilot's safety gate affordable
autopilotCron checks every token it buys against safety.minTokenScore. Without the 6-hour reuse, that would be a model call per token per tick. Trusted majors are skipped entirely; unscored tokens block rather than pass.
The embed badge
GET /v1/safety/token/:address/badge.svg returns a self-contained SVG a project can embed on its own site. The full iframe view is the frontend route /token/:address/embed.
The embed is blocked from third-party iframes on defi
netlify.toml sets X-Frame-Options: SAMEORIGIN for /* with no override. netlify.ai.toml fixes it for /token/*/embed via CSP frame-ancestors * — so the badge only works once the artifact routes live on ai.blazpay.com.
Wallet card — /v1/wallet-card
Route file: v1/routes/walletCard.route.js. Service: v1/services/walletCard.service.js. Model: models/walletCardModel.js.
| Method | Path | Purpose |
|---|---|---|
GET | /v1/wallet-card/:wallet | Cached card JSON |
POST | /v1/wallet-card/generate | Force a fresh generate. Body { wallet } |
Accepts a 0x address, a Blazpay username, or a .eth name.
Data sources
| Source | Provides |
|---|---|
portfolioService.getSummary / getPortfolio (Zerion) | Balances, chain distribution, top tokens |
Etherscan v2 txlist (asc + desc, offset=1) | First and last transaction per chain |
Etherscan v2 eth_getTransactionCount proxy | Per-chain transaction counts |
Chains: Ethereum, BSC, Polygon, Arbitrum, Optimism, Base, Linea, Avalanche.
Labels
Heuristic, no LLM: Whale / Shark / Fish · OG (5y+) / Veteran (3y+) / Newcomer · Multi-chain / Power user / Active · Diamond hands / Degen / Stable stacker.
Approval revoker
There is no backend route
The REVOKE_APPROVALS chat intent returns the scan inline in the message payload, and revokes are client-side transactions. Service: v1/services/approvals.service.js, called from agent.service.js.
Scan
- Etherscan v2
module=logsgetLogs,topic0 = 0x8c5be1…(theApprovalevent),topic1 = pad32(wallet). Paginates up to 5 × 1000 logs. - Reduce to the latest log per
(token, spender). - Keep pairs with value
> 0. - Cap at the top 25 by allowance size.
- Per token:
tokeninfo. Per spender:getsourcecodefor verified flag and name.
Risk score
| Condition | Points |
|---|---|
| Unlimited allowance | +40 |
| Unverified spender | +25 |
| Very large finite allowance | +15 |
The scan infers "active" from the most recent Approval log
It does not read the current on-chain allowance(owner, spender), so a freshly spent allowance can still appear. A multicall over allowance is the clean hardening and is not implemented.
Public frontend pages
Routed outside MainLayout so they render unauthenticated:
| Route | Page |
|---|---|
/report/:id | pages/audit/AuditReport.tsx |
/certificate/:id | pages/audit/AuditCertificate.tsx |
/token/:address | pages/audit/TokenSafety.tsx |
/token/:address/embed | pages/audit/TokenSafetyEmbed.tsx |
/wallet/:wallet | pages/audit/WalletCard.tsx |
SEOHead is applied to all of them, which helps Googlebot. Social bots see the global OG fallback — there is no server-side OG rendering.
Export on the wallet card and certificate lazy-loads html-to-image and jspdf. The exportable region is <div ref={cardRef}>, and elements marked data-export-hide="true" are excluded — so buttons do not appear in the download.
Environment variables
| Variable | Used by |
|---|---|
ETHERSCAN_API_KEY | All four features. Etherscan v2 multi-chain |
OPENAI_API_KEY | Credential for the audit and token-safety models |
AUDIT_PUBLIC_BASE_URL | Share-link prefix |
Wallet card and approval scanning use no LLM.
Failure modes
| Symptom | Cause |
|---|---|
| Certificate 404s | certificateEligible is false — there are critical or high findings |
| Audit by address fails | Source is not verified on that chain's explorer. The user must paste source |
| Partial audit | 40k-character clamp; check the truncation flag |
| Weak token-safety score on a fine token | Unverified source — the score reflects uncertainty, not confirmed danger |
| Badge will not embed | X-Frame-Options on defi. See the warning above |
| Approval list shows a spent allowance | The scan reads logs, not current allowances |
| Approval list looks short | Capped at 25 by size |
| Wallet card shows zero balance | Zerion does not index that chain |
| Shared report unfurls generically | No server-side OG rendering |