Introduction
Nexalix Guard is an anti-sybil scoring API for Telegram Mini Apps and TON projects.
Send a user's tg_user_id plus their signed Telegram initData, get back a
verdict — clean, suspicious, or sybil — with explainable
reasons and a numeric score.
The surface is intentionally small: one scoring endpoint, one feedback endpoint, one health check. Everything interesting happens server-side.
Quickstart · 5 steps
- Get an API key from Nexalix Labs. Format
nxg_<32 base62 chars>. Shown once — store it in your secret manager. - Capture raw
initDatafromTelegram.WebApp.initDataon your client, send it to your own backend. - POST to
/api/v1/scorefrom your backend with the key in theX-API-Keyheader. - Branch on
verdict— letcleanthrough, soft-challengesuspicious, blocksybil. - Send
feedbacklater when you confirm a user as a real human or as a farm — this trains the cross-project graph.
Authentication
Every request to /api/v1/* requires an API key in the X-API-Key header.
The key value is a secret. The first 8 characters after the nxg_ prefix are a non-secret
identifier safe to show in your own logs and UI.
X-API-Key: nxg_abc12345xyz... - Soft limit: up to 10 active keys per merchant. Rotate by issuing a new key, then revoking the old one.
- Keys are stored as Argon2id hashes — even Nexalix cannot recover the raw value.
- Compromised key? Email security@nexalix.io for immediate revocation.
Base URL & versioning
| Environment | Base URL |
|---|---|
| Production | https://guard.nexalix.io/api |
| Local development | http://localhost:3000/api |
Version lives in the URL path (/api/v1/...). Backwards-incompatible changes get a new
major version. Additive changes within a major version never silently change response semantics.
Track changes in the changelog section.
POST /v1/score
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
tg_user_id | integer (int64) | yes | Positive Telegram user ID. |
init_data | string | yes | Raw Telegram.WebApp.initData. HMAC-SHA256 verified server-side. Max 8192 chars. |
wallet_address | string | no | TON wallet address if the user has connected one. |
event | enum | no | signup · claim_points · withdraw · custom |
ip | string | no | End-user IP if your backend can pass it. |
user_agent | string | no | End-user User-Agent header. |
Example request
curl -X POST https://guard.nexalix.io/api/v1/score \
-H "X-API-Key: $NEXALIX_GUARD_KEY" \
-H "Content-Type: application/json" \
-d '{
"tg_user_id": 123456789,
"init_data": "query_id=AAH...&user=%7B%22id%22%3A123456789...&auth_date=1747900000&hash=...",
"wallet_address": "EQDrjaLahLkMB-...-Tt2",
"event": "claim_points"
}' 200 response
{
"score": 55,
"verdict": "suspicious",
"reasons": [
{ "code": "tg_account_age_lt_7d", "weight": 35 },
{ "code": "tg_no_username", "weight": 20 }
],
"request_id": "01J4F7K2P8X9V6T2Q5R8N0M3W1"
}
Keep the request_id — you'll reference it when sending feedback. It's also returned on
every error response for support requests.
Status codes
| Status | When |
|---|---|
| 200 | Verdict computed and persisted. |
401 missing_api_key | No X-API-Key header. |
401 invalid_api_key | Key not recognized or revoked. |
401 invalid_init_data | HMAC mismatch, expired auth_date, or tampered payload. |
| 413 | Body exceeds the size limit. |
422 VALIDATION_FAILED | Payload doesn't match the schema (e.g. missing tg_user_id). |
503 SERVICE_UNAVAILABLE | Database or Redis unreachable — retry with backoff. |
POST /v1/feedback
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
request_id | string | yes | The request_id from a prior /v1/score response. |
kind | enum | yes | confirmed_sybil · confirmed_clean · unknown |
comment | string | no | Free text · max 2000 chars. |
Example request
curl -X POST https://guard.nexalix.io/api/v1/feedback \
-H "X-API-Key: $NEXALIX_GUARD_KEY" \
-H "Content-Type: application/json" \
-d '{
"request_id": "01J4F7K2P8X9V6T2Q5R8N0M3W1",
"kind": "confirmed_sybil",
"comment": "Wallet drained on first claim, never returned"
}' 200 response
{
"feedback_id": "a1b2c3d4-...",
"request_id": "01J4F7K2P8X9V6T2Q5R8N0M3W1"
} Status codes
| Status | When |
|---|---|
| 200 | Feedback recorded. |
| 401 | Authentication failed (same codes as /score). |
| 404 | request_id not found, or belongs to a different merchant. |
409 CONFLICT | Feedback for this request_id already exists. Each request can receive feedback exactly once. |
| 422 | Invalid kind or oversized comment. |
| 503 | Database unreachable. |
GET /health
GET /health → 200 OK
{ "status": "ok", "version": "0.0.1" }
Wire this into your orchestrator's healthcheck. Extra headers (including a valid X-API-Key) are
ignored — health is intentionally unauthenticated.
Verdict semantics
The score is a deterministic weighted sum, 0–100, clamped at both ends. Each triggered rule contributes its weight. The thresholds:
| Score range | Verdict | Recommended action |
|---|---|---|
0–39 | clean | Let the action proceed. |
40–69 | suspicious | Soft challenge — extra captcha, delayed reward, manual review. |
70–100 | sybil | Block the action. Optional appeal flow. |
A rule may also raise a hard-block flag (e.g. known sybil cluster, blacklisted wallet).
Hard-block forces verdict = "sybil" regardless of the numeric score. The score in the
response still reflects the weighted sum so you can see why.
Reason codes
Each entry in the reasons array has a stable code string and the
weight it contributed. Codes do not change semantics within a major API version —
you can safely match on them in your own logic and dashboards.
The first batch lands in Week 2 of the MVP:
More codes land as we add TON on-chain signals (Week 3) and behavioural signals (Week 4). Each new code is documented here before it ships.
Error envelope
Every error response, regardless of endpoint or status, has the same shape:
{
"error": {
"code": "invalid_api_key",
"message": "API key not recognized or revoked",
"details": { /* optional, structured */ }
},
"request_id": "01J4F7K2P8X9V6T2Q5R8N0M3W1"
}
The request_id field appears in every response — success or failure. Keep it in your
logs; quoting it lets us trace any incident on our side.
Limits & retention
- Request body size — up to 1 MB.
init_dataalone is capped at 8 KB. - Active API keys — up to 10 per merchant. Need more? Email us.
- Score request retention — 90 days hot, then soft-deleted. Feedback windows count from the original request timestamp.
- Rate limits — none enforced during pilot. Production rate limits will be per-merchant and disclosed at least 14 days before activation.
- Payload privacy — we store a strict whitelist of fields from each request (parsed Telegram user id, auth date, platform, language, country code, user-agent, request id). Raw
init_data, HMAC hashes, and any field outside that whitelist are never persisted.
Changelog
Public API changes are tracked in the repo's CHANGELOG.md (link active when the repo
is published). Highlights:
- v0.2.0-dev — first end-to-end implementation of
/v1/scoreand/v1/feedback; explicit whitelist payload snapshot; Argon2id key hashing; 90-day retention.
Questions, missing docs, or want a key? Email hello@nexalix.io.