Skip to main content

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:

  1. Base URLhttps://tokin-api.dedaub.com
  2. Path/token/{chain}/{token_address} (chain is a name, not a numeric chain id — see §6)
  3. AuthX-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 paramValuesDefaultEffect
response_formatdedaub | goplusdedaubgoplus 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:

  1. Fields are nested under features, not under result["<address>"]. Update every accessor: body.is_open_sourcebody.features.is_open_source.
  2. Errors come via HTTP status code + error string, not an in-body code/message pair. On non-2xx, features is null and error carries 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:

  1. Locate the GoPlus call sites. Grep for gopluslabs.io, token_security, goplus, and known SDK imports (e.g. goplus-sdk-py, @goplus/sdk-node).
  2. Identify the auth mechanism in use. GoPlus integrations vary: anonymous, ?api_key=, or app-id/secret signed access tokens. All three collapse to a single X-API-Key header in Tok{In}.
  3. 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.
  4. Update the URL builder and add the X-API-Key header. Append ?response_format=goplus if the existing parser relies on GoPlus field names and "1"/"0" strings — otherwise migrate to the native dedaub schema in the same change.
  5. 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-null fields (§4) — if downstream code branches on them, simplify the branch or wire them to a different signal.
  6. Update error handling. Tok{In} returns standard HTTP codes (§7); GoPlus historically returned 200 with a code field in the body. Replace body-code checks with HTTP status checks.
  7. Update rate-limit handling. Tok{In} emits standard Retry-After plus X-RateLimit-* and X-Quota-* headers (§8). Wire any retry/backoff logic to those headers.
  8. 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)GoPlusNotes
is_open_sourceis_open_sourcebool → "1"/"0"
is_proxyis_proxybool → "1"/"0"
mint_or_burn_functionis_mintablebool → "1"/"0"
owner_addressowner_addressstring
can_selfdestructselfdestructbool → "1"/"0"
external_callexternal_callbool → "1"/"0"
is_in_dexis_in_dexbool → "1"/"0"
receive_taxbuy_taxdecimal as string
send_taxsell_taxdecimal as string
cannot_buycannot_buybool → "1"/"0"
tax_can_be_modifiedslippage_modifiablebool → "1"/"0"
trading_cooldowntrading_cooldownbool → "1"/"0"
creator_addresscreator_addressstring
creator_percentcreator_percentdecimal as string
is_launchpad_tokenlaunchpad_tokenbool → "1"/"0"

Tax direction is inverted. send_taxsell_tax and receive_taxbuy_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 that holder_count itself is null (see below) but the holders[] array is populated.

Computed mappings (derived, not 1:1)

GoPlus fieldDerived fromLogic
transfer_pausablecannot_buy, pause_status_can_be_modifiedTrue if either is True
is_blacklistedhas_blacklist_or_whitelistsame value (temporary)
is_whitelistedhas_blacklist_or_whitelistsame value (temporary)
is_anti_whalehas_trading_cap, has_position_capTrue if either is True
anti_whale_modifiabletrading_cap_can_be_modified, position_cap_can_be_modifiedTrue 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_owner
  • cannot_sell_all
  • can_take_back_ownership
  • owner_change_balance
  • gas_abuse
  • personal_slippage_modifiable
  • lp_total_supply
  • is_airdrop_scam
  • trust_list
  • holder_count
  • owner_balance
  • owner_percent
  • is_honeypot (not listed in upstream public docs but always returns null today)

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
ethereum1
binance56
base8453
arbitrum42161
avalanche43114

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

CodeMeaning
200Success
401Missing or invalid API key
422Validation error: malformed EVM address or burn/null address (e.g. 0x000…000, 0xdead…dead)
429Rate limit or quota exceeded
500Internal 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:

TierHeaders on 429Retry-After?
Per-minute QPSRetry-After, X-RateLimit-Limit, X-RateLimit-Remainingyes
Daily quotaRetry-After, X-Quota-Daily-Limit, X-Quota-Daily-Usedyes
Monthly quotaX-Quota-Monthly-Limit, X-Quota-Monthly-Usedno

Two practical implications:

  • Retry strategy must branch on which header set is present. Per-minute and daily 429s have a Retry-After you 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 under features (see §2.1). This is the most common silently-wrong outcome of an unreviewed AI migration.
  • Trusting is_honeypot as a kill switch. It always returns null today (§4). GoPlus integrations that gate trades on is_honeypot === "1" will treat every token as safe after migration. Replace with a derived signal (cannot_buy, per-pool dex[].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-null fields without a guard (see §4). If a GoPlus integration relies on holder_count or owner_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 as sell_tax/buy_tax until the GoPlus mapper renames them. Note also that buy_tax/sell_tax may 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=goplus with 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.