AGENTS.md — Migrating from GoPlus to Tok{In}
This file gives AI coding agents (Claude Code, Cursor, Copilot, etc.) the context they need to migrate an existing GoPlus token-security integration over to Dedaub's Tok{In} API with minimal code changes.
Source of truth: https://docs.dedaub.com/docs/token_safety/tokin/
If you are an AI agent helping a developer migrate, read this file end-to-end before editing their code. The sections are ordered so that the first three give you everything needed for a one-line drop-in swap; the rest covers field-level differences, edge cases, and known gaps.
1. TL;DR — the one-line swap
GoPlus's token_security endpoint can be replaced by Tok{In} with three changes:
- Base URL →
https://tokin-api.dedaub.com - Path →
/token/{chain}/{token_address}(chain is a name, not a numeric chain id — see §6) - Auth →
X-API-Key: <YOUR-API-KEY>header (instead of GoPlus's?api_key=query param or app-id/secret signing)
Append ?response_format=goplus to receive field values in GoPlus's schema with "1"/"0" string-boolean encoding. The outer envelope is different (see §2.1) — fields live under features, not at the top level — so a literal drop-in is rarely possible without one small parser change.
# Before (GoPlus)
curl 'https://api.gopluslabs.io/api/v1/token_security/1?contract_addresses=0xABC...'
# After (Tok{In}, GoPlus-compatible response)
curl -H 'X-API-Key: YOUR-API-KEY' \
'https://tokin-api.dedaub.com/token/ethereum/0xABC...?response_format=goplus'
2. Endpoint reference
GET https://tokin-api.dedaub.com/token/{chain}/{token_address}
GET https://tokin-api.dedaub.com/token/{chain}/{token_address}?response_format=goplus
Headers:
X-API-Key: <required>
| Query param | Values | Default | Effect |
|---|---|---|---|
response_format | dedaub | goplus | dedaub | goplus reshapes the body to GoPlus's schema and string-boolean encoding. |
The native dedaub schema is richer (typed booleans, additional fields, structured tax info). Use it for greenfield code; use goplus only when you are migrating existing GoPlus parsers and want to defer schema changes.
2.1 Response envelope (read this before parsing)
Tok{In} always wraps the result in this envelope, regardless of response_format:
{
"chain": "ethereum",
"contract_address": "0xA0b8...eB48",
"features": { /* SafetyFeatures (dedaub) | GoPlusMappedFeatures (goplus) */ },
"error": ""
}
This differs from GoPlus's { "code": 1, "message": "OK", "result": { "0xABC...": { ... } } } shape in two ways that matter to a migrating client:
- Fields are nested under
features, not underresult["<address>"]. Update every accessor:body.is_open_source→body.features.is_open_source. - Errors come via HTTP status code +
errorstring, not an in-bodycode/messagepair. On non-2xx,featuresisnullanderrorcarries the human-readable reason.
Full response schema: see tokin.md.
3. Migration steps for an AI agent
When asked to migrate a codebase from GoPlus to Tok{In}, follow this sequence:
- Locate the GoPlus call sites. Grep for
gopluslabs.io,token_security,goplus, and known SDK imports (e.g.goplus-sdk-py,@goplus/sdk-node). - Identify the auth mechanism in use. GoPlus integrations vary: anonymous,
?api_key=, or app-id/secret signed access tokens. All three collapse to a singleX-API-Keyheader in Tok{In}. - Translate the chain identifier. GoPlus uses numeric chain IDs in the path (
/token_security/1); Tok{In} uses names (/token/ethereum/...). See §6 for the full mapping. If the codebase passes chain IDs around as integers, add a small lookup helper rather than rewriting every call site. - Update the URL builder and add the
X-API-Keyheader. Append?response_format=goplusif the existing parser relies on GoPlus field names and"1"/"0"strings — otherwise migrate to the nativededaubschema in the same change. - Audit field reads. Cross-reference every field the code reads against §4. Pay attention to:
- Computed fields (
transfer_pausable,is_anti_whale, etc.) — these are derived, not 1:1. - Always-
nullfields (§4) — if downstream code branches on them, simplify the branch or wire them to a different signal.
- Computed fields (
- Update error handling. Tok{In} returns standard HTTP codes (§7); GoPlus historically returned
200with acodefield in the body. Replace body-codechecks with HTTP status checks. - Update rate-limit handling. Tok{In} emits standard
Retry-AfterplusX-RateLimit-*andX-Quota-*headers (§8). Wire any retry/backoff logic to those headers. - Run the test suite. If there isn't one for token-security calls, add at least one integration test against a known token (e.g. USDC on Ethereum:
0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48) before declaring the migration done.
Do not silently delete GoPlus fields the user's code reads — flag them as TODOs (especially the always-null set in §4) so the developer can decide what to do.
4. Field mapping (Tok{In} dedaub → GoPlus)
When response_format=goplus, the server applies these mappings. All booleans become the strings "1" (true) or "0" (false).
Direct mappings
| Tok{In} (Dedaub) | GoPlus | Notes |
|---|---|---|
is_open_source | is_open_source | bool → "1"/"0" |
is_proxy | is_proxy | bool → "1"/"0" |
mint_or_burn_function | is_mintable | bool → "1"/"0" |
owner_address | owner_address | string |
can_selfdestruct | selfdestruct | bool → "1"/"0" |
external_call | external_call | bool → "1"/"0" |
is_in_dex | is_in_dex | bool → "1"/"0" |
receive_tax | buy_tax | decimal as string |
send_tax | sell_tax | decimal as string |
cannot_buy | cannot_buy | bool → "1"/"0" |
tax_can_be_modified | slippage_modifiable | bool → "1"/"0" |
trading_cooldown | trading_cooldown | bool → "1"/"0" |
creator_address | creator_address | string |
creator_percent | creator_percent | decimal as string |
is_launchpad_token | launchpad_token | bool → "1"/"0" |
⚠ Tax direction is inverted. send_tax → sell_tax and receive_tax → buy_tax. Make sure any code computing slippage matches the renamed semantic.
⚠ buy_tax / sell_tax are pool-aware. The mapper first walks features.dex[] and uses the first pool with non-null buy_tax / sell_tax, falling back to the contract-level receive_tax / send_tax only if no pool reports a value. The result is usually closer to real swap behavior than GoPlus's contract-level tax, but it can disagree with the contract-level fields in the same response.
Additional fields populated in goplus mode
Beyond the GoPlus-spec fields, the response also carries:
token_name,token_symbol,total_supply— populated when known.holders[]— array of{ address, balance, percent }. Note thatholder_countitself isnull(see below) but theholders[]array is populated.
Computed mappings (derived, not 1:1)
| GoPlus field | Derived from | Logic |
|---|---|---|
transfer_pausable | cannot_buy, pause_status_can_be_modified | True if either is True |
is_blacklisted | has_blacklist_or_whitelist | same value (temporary) |
is_whitelisted | has_blacklist_or_whitelist | same value (temporary) |
is_anti_whale | has_trading_cap, has_position_cap | True if either is True |
anti_whale_modifiable | trading_cap_can_be_modified, position_cap_can_be_modified | True if either is True |
⚠ Blacklist/whitelist ambiguity. Both flags map from the same source, so the API cannot distinguish blacklist-only, whitelist-only, or both. Code that branches on one vs. the other should be reviewed.
Currently always null
These GoPlus fields exist in the response shape but are not yet tracked by Tok{In}. If downstream code reads them, treat the read as a TODO and either remove the dependency or substitute a different signal:
hidden_ownercannot_sell_allcan_take_back_ownershipowner_change_balancegas_abusepersonal_slippage_modifiablelp_total_supplyis_airdrop_scamtrust_listholder_countowner_balanceowner_percentis_honeypot(not listed in upstream public docs but always returnsnulltoday)
⚠ is_honeypot is the field GoPlus consumers most often gate trades on. Migration should not treat its absence as "not a honeypot" — derive an equivalent signal from cannot_buy, the per-pool dex[].can_buy/can_sell (in dedaub mode), and tax fields, or block on the field being unavailable.
For the full GoPlus mapping (direct, computed, and always-null fields), see tokin.md.
5. Code examples
Python (requests)
import os, requests
API_KEY = os.environ["TOKIN_API_KEY"]
BASE = "https://tokin-api.dedaub.com"
def token_security(chain: str, address: str, *, goplus_compat: bool = True) -> dict:
params = {"response_format": "goplus"} if goplus_compat else None
r = requests.get(
f"{BASE}/token/{chain}/{address}",
params=params,
headers={"X-API-Key": API_KEY},
timeout=30,
)
if r.status_code == 429:
retry_after = int(r.headers.get("Retry-After", "60"))
raise RuntimeError(f"rate limited; retry after {retry_after}s")
r.raise_for_status()
return r.json()
TypeScript (fetch)
const TOKIN_BASE = "https://tokin-api.dedaub.com";
export async function tokenSecurity(
chain: "ethereum" | "binance" | "base" | "arbitrum" | "avalanche",
address: string,
{ goplusCompat = true } = {},
) {
const url = new URL(`${TOKIN_BASE}/token/${chain}/${address}`);
if (goplusCompat) url.searchParams.set("response_format", "goplus");
const res = await fetch(url, {
headers: { "X-API-Key": process.env.TOKIN_API_KEY! },
});
if (res.status === 429) {
const retryAfter = Number(res.headers.get("Retry-After") ?? 60);
throw new Error(`rate limited; retry after ${retryAfter}s`);
}
if (!res.ok) throw new Error(`tokin ${res.status}: ${await res.text()}`);
return res.json();
}
Go (net/http)
Idiomatic client with context.Context, wrapped errors, and a typed ErrRateLimited so callers can branch on errors.As and respect Retry-After:
package tokin
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"os"
"strconv"
"time"
)
// ErrRateLimited is returned when Tok{In} responds with HTTP 429.
// Callers should sleep for RetryAfter before retrying.
type ErrRateLimited struct{ RetryAfter time.Duration }
func (e *ErrRateLimited) Error() string {
return fmt.Sprintf("tokin rate limited; retry after %s", e.RetryAfter)
}
// Client is a minimal Tok{In} HTTP client. Zero value is not usable; call NewClient.
type Client struct {
APIKey string
BaseURL string
HTTP *http.Client
}
// NewClient builds a Client with sensible defaults. APIKey is read from TOKIN_API_KEY.
func NewClient() *Client {
return &Client{
APIKey: os.Getenv("TOKIN_API_KEY"),
BaseURL: "https://tokin-api.dedaub.com",
HTTP: &http.Client{Timeout: 30 * time.Second},
}
}
// TokenSecurity fetches token-safety analysis for the given chain and address.
// When goplusCompat is true, the response is reshaped to GoPlus's schema with
// "1"/"0" string booleans.
func (c *Client) TokenSecurity(ctx context.Context, chain, address string, goplusCompat bool) (map[string]any, error) {
u, err := url.Parse(fmt.Sprintf("%s/token/%s/%s", c.BaseURL, chain, address))
if err != nil {
return nil, fmt.Errorf("build url: %w", err)
}
if goplusCompat {
q := u.Query()
q.Set("response_format", "goplus")
u.RawQuery = q.Encode()
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return nil, fmt.Errorf("new request: %w", err)
}
req.Header.Set("X-API-Key", c.APIKey)
resp, err := c.HTTP.Do(req)
if err != nil {
return nil, fmt.Errorf("do request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusTooManyRequests {
secs, _ := strconv.Atoi(resp.Header.Get("Retry-After"))
if secs <= 0 {
secs = 60
}
return nil, &ErrRateLimited{RetryAfter: time.Duration(secs) * time.Second}
}
if resp.StatusCode >= 400 {
return nil, fmt.Errorf("tokin %d", resp.StatusCode)
}
var out map[string]any
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
return nil, fmt.Errorf("decode body: %w", err)
}
return out, nil
}
For typed access, replace map[string]any with a generated struct that mirrors the response shape documented in tokin.md.
Rust (reqwest + tokio)
# Cargo.toml
[dependencies]
reqwest = { version = "0.12", features = ["json"] }
serde_json = "1"
thiserror = "1"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
use std::env;
use std::time::Duration;
use reqwest::{Client, StatusCode};
use serde_json::Value;
use thiserror::Error;
const BASE: &str = "https://tokin-api.dedaub.com";
#[derive(Debug, Error)]
pub enum TokinError {
#[error("rate limited; retry after {0:?}")]
RateLimited(Duration),
#[error("tokin {status}: {body}")]
Http { status: u16, body: String },
#[error(transparent)]
Request(#[from] reqwest::Error),
#[error("missing TOKIN_API_KEY")]
MissingKey,
}
/// Fetch token-safety analysis. When `goplus_compat` is true, the response uses
/// GoPlus's schema with `"1"`/`"0"` string booleans.
pub async fn token_security(
chain: &str,
address: &str,
goplus_compat: bool,
) -> Result<Value, TokinError> {
let api_key = env::var("TOKIN_API_KEY").map_err(|_| TokinError::MissingKey)?;
let url = format!("{BASE}/token/{chain}/{address}");
let mut req = Client::new()
.get(&url)
.header("X-API-Key", api_key)
.timeout(Duration::from_secs(30));
if goplus_compat {
req = req.query(&[("response_format", "goplus")]);
}
let resp = req.send().await?;
if resp.status() == StatusCode::TOO_MANY_REQUESTS {
let secs = resp
.headers()
.get("Retry-After")
.and_then(|v| v.to_str().ok())
.and_then(|s| s.parse::<u64>().ok())
.unwrap_or(60);
return Err(TokinError::RateLimited(Duration::from_secs(secs)));
}
let status = resp.status();
if !status.is_success() {
let body = resp.text().await.unwrap_or_default();
return Err(TokinError::Http { status: status.as_u16(), body });
}
Ok(resp.json().await?)
}
For typed access, replace serde_json::Value with a #[derive(Deserialize)] struct mirroring the GoPlus schema (or the native Dedaub shape documented in tokin.md).
Chain-id translation helper
If existing GoPlus code carries numeric chain ids:
const CHAIN_ID_TO_NAME: Record<number, string> = {
1: "ethereum",
56: "binance",
8453: "base",
42161: "arbitrum",
43114: "avalanche",
};
6. Supported chains
| Name (use in path) | Chain ID |
|---|---|
ethereum | 1 |
binance | 56 |
base | 8453 |
arbitrum | 42161 |
avalanche | 43114 |
Any other chain GoPlus supports (Polygon, Optimism, Solana, etc.) is not currently available on Tok{In} — surface this as a blocker rather than silently dropping the call.
7. HTTP response codes
| Code | Meaning |
|---|---|
| 200 | Success |
| 401 | Missing or invalid API key |
| 422 | Validation error: malformed EVM address or burn/null address (e.g. 0x000…000, 0xdead…dead) |
| 429 | Rate limit or quota exceeded |
| 500 | Internal server error (or upstream analysis failure) |
On 422 and 500 the envelope is still {chain, contract_address, features: null, error: "<reason>"} — the error string is the source of the human-readable reason.
GoPlus's habit of returning 200 with an in-body code field does not apply here — branch on HTTP status instead.
8. Rate limits & quotas
There are three independent enforcement tiers, each emitting its own header set on 429:
| Tier | Headers on 429 | Retry-After? |
|---|---|---|
| Per-minute QPS | Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining | yes |
| Daily quota | Retry-After, X-Quota-Daily-Limit, X-Quota-Daily-Used | yes |
| Monthly quota | X-Quota-Monthly-Limit, X-Quota-Monthly-Used | no |
Two practical implications:
- Retry strategy must branch on which header set is present. Per-minute and daily 429s have a
Retry-Afteryou can sleep on; a monthly-quota 429 means "wait until next billing period" and there is no header telling you when. Treat that case as fatal for the current period. - Headers are emitted only on the failing 429, not on successful responses. Clients cannot proactively monitor remaining budget — only learn about it on rejection. Pace requests defensively rather than racing the budget.
9. Common migration pitfalls
- Reading fields at the wrong nesting level. GoPlus puts fields under
result["<address>"]; Tok{In} puts them underfeatures(see §2.1). This is the most common silently-wrong outcome of an unreviewed AI migration. - Trusting
is_honeypotas a kill switch. It always returnsnulltoday (§4). GoPlus integrations that gate trades onis_honeypot === "1"will treat every token as safe after migration. Replace with a derived signal (cannot_buy, per-pooldex[].can_buy/can_sell, tax checks) or hard-block when the field is unavailable. - Treating
"1"/"0"as truthy/falsy in JS. The string"0"is truthy. Use explicit=== "1"checks or convert at the boundary. - Reading always-
nullfields without a guard (see §4). If a GoPlus integration relies onholder_countorowner_percent, they must be sourced elsewhere or the feature degraded. - Forgetting the tax inversion.
send_tax/receive_tax(Tok{In} native) are not the same direction assell_tax/buy_taxuntil the GoPlus mapper renames them. Note also thatbuy_tax/sell_taxmay come from per-pool DEX data (§4) and disagree with the contract-level tax. - Hard-coding chain IDs in the URL. Tok{In} uses chain names; failing to translate produces 422s, not 404s.
- Confusing
response_format=gopluswith the data being GoPlus's data. The schema is GoPlus-shaped, but the underlying analysis is Dedaub's (Gigahorse-based bytecode analysis). Detection coverage and false-positive characteristics will differ.
10. When to drop GoPlus compatibility mode
response_format=goplus is a migration aid, not the long-term recommendation. Once the rest of the integration is stable, prefer the native dedaub schema for:
- typed booleans (no
"1"/"0"parsing), - richer tax structure,
- access to fields not representable in the GoPlus shape (e.g. structured cap modification flags),
- forward compatibility as new detectors land.
The native response shape and field reference are documented in tokin.md.