Public KOL Identity
Resolve source-asserted X and Telegram identities in either direction: from a Solana wallet to its public aliases, or from a public handle to every associated wallet.
Identity records are curated, source-asserted associations. They are useful attribution metadata, but they are not cryptographic proof that a social-account owner controls a wallet. Keep the wallet address visible and preserve source and verification when presenting or exporting an association.
Both endpoints require a Layer 2 API key and the standard Bearer header.
Fomo-sourced KOL wallet data
Conyr indexes public X handle-to-wallet mappings surfaced by Fomo’s trader leaderboards alongside other curated identity sources. Fomo-derived aliases are returned with source: "fomo", so terminals, agents, and exports can preserve that provenance explicitly. Because source is attached to each alias, inspect it per record instead of assuming every registry result came from Fomo.
Choose the right endpoint
| Starting point | Endpoint | Use it for |
|---|---|---|
| Solana wallet | GET /v1/wallet/{address}/identity | Preferred identity, platform-correct profile URL, provenance, timestamps, and every active alias |
| X or Telegram handle | GET /v1/kol-wallets/{username} | Discovering every wallet associated with an exact handle |
| Wallet analytics | GET /v1/wallet/{address}/labels | Classification plus compact nullable kol_username / kol_platform enrichment |
| Ranked traders | GET /v1/leaderboard | Performance rows with compact nullable KOL enrichment |
Use /identity as the canonical identity read. The labels endpoint is backed by the behavioral snapshot, so an alias-only wallet can have a valid identity while /labels still returns JSON null.
Identity object
primary and each item in aliases use this contract:
| Field | Type | Meaning |
|---|---|---|
platform | "x" | "telegram" | Platform the username belongs to |
username | string | Display handle without a leading @ |
profile_url | URL | Platform-correct x.com or t.me URL; prefer this over constructing a URL client-side |
source | string | Registry provenance label, such as "fomo" for mappings sourced from Fomo’s public trader leaderboards; treat it as metadata rather than a verification level |
verification | "source_asserted" | Fixed provenance state: the registry source asserted the association; not proof of wallet ownership |
created_at | ISO 8601 datetime | When the association was added |
updated_at | ISO 8601 datetime | When the association was last changed |
X aliases take precedence over Telegram aliases for primary. Within a platform, the most recently updated active alias wins. When aliases is non-empty, primary is a copy of its first item; it is not an additional association.
Wallet to identity
GET /v1/wallet/{address}/identity
Returns the preferred public identity and every active alias independently of wallet trading history.
Parameters
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
address | path | string | yes | 32–44 characters from the Solana base58 alphabet; route-level syntax check |
Example
curl -sS \
-H "Authorization: Bearer $API_KEY" \
"https://api.conyr.ai/v1/wallet/$WALLET_ADDRESS/identity"Response with aliases
{
"wallet_address": "A5SEXYJY4jTEi6sjMLfZs5KAP8SVFvLDPDV67GgSSZSk",
"primary": {
"platform": "x",
"username": "frankdegods",
"profile_url": "https://x.com/frankdegods",
"source": "fomo",
"verification": "source_asserted",
"created_at": "2026-08-20T18:30:00Z",
"updated_at": "2026-08-25T09:15:00Z"
},
"aliases": [
{
"platform": "x",
"username": "frankdegods",
"profile_url": "https://x.com/frankdegods",
"source": "fomo",
"verification": "source_asserted",
"created_at": "2026-08-20T18:30:00Z",
"updated_at": "2026-08-25T09:15:00Z"
}
]
}Unknown wallet identity
A syntactically accepted wallet identifier with no active association is not an error. It returns HTTP 200 with a stable empty object:
{
"wallet_address": "11111111111111111111111111111111",
"primary": null,
"aliases": []
}Do not turn primary: null into “not a KOL” or “verified anonymous.” It means only that the active registry has no association for that wallet.
Handle to wallets
GET /v1/kol-wallets/{username}
Returns every active wallet associated with an exact, case-insensitive username.
Parameters
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
username | path | string | yes | Optional leading @; 1–32 ASCII letters, digits, or _ after normalization |
platform | query | x | telegram | no | Restricts the match to one platform |
Use platform whenever the user’s starting context identifies the platform. Without it, the response can combine wallets for the same username on X and Telegram, while the compact response does not identify which platform produced each match.
Examples
# X handle; leading @ is optional
curl -sS \
-H "Authorization: Bearer $API_KEY" \
"https://api.conyr.ai/v1/kol-wallets/frankdegods?platform=x"
# Telegram handle
curl -sS \
-H "Authorization: Bearer $API_KEY" \
"https://api.conyr.ai/v1/kol-wallets/DecusCalls?platform=telegram"Response
{
"username": "frankdegods",
"wallets": [
"7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
"A5SEXYJY4jTEi6sjMLfZs5KAP8SVFvLDPDV67GgSSZSk"
]
}An unknown handle returns HTTP 200 with an empty wallets array:
{
"username": "unknown_handle",
"wallets": []
}Wallets are returned in deterministic address order, not by recency, confidence, performance, or “main wallet” status. Do not treat the first address as canonical.
The response username is normalized to lowercase and never includes @. Preserve the user’s original display text separately if casing matters to your product.
Practical workflow: handle to attributed wallets
Handle lookup intentionally returns only addresses. Fetch /identity for each result when the UI or agent needs a platform-correct link, provenance, or timestamps:
const base = "https://api.conyr.ai/v1";
const headers = { Authorization: `Bearer ${apiKey}` };
const handle = input.trim().replace(/^@+/, "");
const lookupResponse = await fetch(
`${base}/kol-wallets/${encodeURIComponent(handle)}?platform=x`,
{ headers },
);
if (!lookupResponse.ok) throw new Error(`lookup: ${lookupResponse.status}`);
const lookup = await lookupResponse.json();
const hydrated: Array<
{ wallet: string; identity?: unknown; error?: string }
> = [];
// Bound concurrency and preserve per-wallet failures.
for (let offset = 0; offset < lookup.wallets.length; offset += 5) {
const wallets = lookup.wallets.slice(offset, offset + 5);
const batch = await Promise.allSettled(
wallets.map(async (wallet: string) => {
const response = await fetch(
`${base}/wallet/${encodeURIComponent(wallet)}/identity`,
{ headers },
);
if (!response.ok) throw new Error(`Conyr ${response.status}`);
return response.json();
}),
);
batch.forEach((result, index) => {
const wallet = wallets[index];
hydrated.push(
result.status === "fulfilled"
? { wallet, identity: result.value }
: { wallet, error: String(result.reason) },
);
});
}Limit hydration concurrency if a handle maps to many wallets; every /identity read counts against the Layer 2 request limit.
The alias used for reverse lookup is not guaranteed to be the hydrated wallet’s primary. Confirm that the normalized (platform, username) still appears in aliases: another active X alias may have higher priority, and the registry can change between the lookup and hydration requests.
For a performance view, join these identity results to /wallet/{address}/performance, /quality, or /trades by the exact wallet address. Do not aggregate multiple wallets into one PnL total unless the product explicitly communicates that grouping and its time window.
Platform-safe rendering
- Display usernames with
@, but store and compare the returned username without it. - Prefer the returned
profile_urlfrom/identity. - For compact
kol_username/kol_platformfields, usehttps://x.com/{username}only forxandhttps://t.me/{username}only fortelegram. - If either compact field is
null, keep the wallet address as the fallback and do not guess the platform. - Keep personal/user-defined wallet names separate from the public identity so one does not overwrite the other.
Status and freshness semantics
| Status | Meaning |
|---|---|
200 | Found association, stable empty result, or unknown handle depending on the response body |
400 | Wallet identifier outside accepted base58-form syntax, invalid username, or invalid platform |
401 | Missing or invalid API key |
403 | API key does not include Layer 2 |
429 | Per-key request limit exceeded; back off before retrying |
500 | Identity registry or service failure |
503 | Authentication or another required backend is temporarily unavailable |
Both direct identity routes query active Postgres registry rows at request time and are not response-cached by the API. Compact KOL fields on /labels and /leaderboard are projections through a ClickHouse dictionary with a 30–60 second refresh lifetime; those endpoint responses can then remain cached for another 60 seconds.
The wallet-label WebSocket channel is activity-driven. An identity-registry update alone does not guarantee a label event for a dormant wallet. In its payload, empty-string kol_username / kol_platform values explicitly clear cached identity; REST compact projections use JSON null. Read /identity when current identity state matters; use the stream only as opportunistic enrichment.
See Wallet Intelligence for behavioral labels and performance endpoints, and Leaderboard for compact identity fields on ranked wallets.