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
hidden_ownerallowance_bypass, privileged_spendersTrue when the bypass probe named at least one address
owner_change_balanceowner_can_change_balancesame value

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.

hidden_owner is tri-state underneath. It maps from the allowance-bypass probe, which simulates transferFrom from candidate callers against a control caller that provably holds no approval. It is "1" only in the address-specific case — a universal bypass (allowance_bypass true, privileged_spenders empty) means the token authorises nobody in particular, which is a broken ERC20 rather than a hidden owner, so it maps to "0". It is null when the probe could not run, which is not the same as "0". Treat null as unknown, never as safe. In dedaub mode read allowance_bypass and privileged_spenders directly — the latter carries the actual addresses that can drain a holder.

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:

  • cannot_sell_all
  • can_take_back_ownership
  • 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[].buy_success/sell_success (in dedaub mode), and tax fields, or block on the field being unavailable.

⚠⚠ buy_success / sell_success are not booleans. They are four-valued: true, false, "pending" (simulation still running) and null (no simulation was attempted for that pool — common for bonding-curve pools). null and "pending" mean unknown and carry no signal. A honeypot check written as if (!pool.sell_success) will flag every healthy token whose pool was not simulated. Test === false explicitly. See tokin.md.

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" | "robinhood",
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",
4663: "robinhood",
};

6. Supported chains

Name (use in path)Chain ID
ethereum1
binance56
base8453
arbitrum42161
avalanche43114
robinhood (New)4663

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.

Robinhood Chain (4663) has no GoPlus equivalent. It is a Tok{In}-only chain, so a migration will have no existing call site to translate — but if the codebase enumerates supported chains as a closed list (union type, enum, allowlist), add robinhood to it rather than leaving 4663 to fall through to the unsupported-chain path.


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[].buy_success/sell_success, tax checks) or hard-block when the field is unavailable.
  • Treating a per-pool null as a failed trade. dex[].buy_success / sell_success return null when a pool was never simulated and "pending" while one is running — neither is a "no". Falsy checks turn both into "cannot sell", which is the inverse of the truth for a perfectly tradeable token (§4).
  • 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.
  • Reading hidden_owner === "0" as "no hidden owner". It is null whenever the allowance-bypass probe could not run, and "0" for a universal bypass, which is worse rather than better (§4). Anything gating on drain risk should read allowance_bypass and privileged_spenders in dedaub mode.
  • 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.