API reference · v1 · updated 28 Sep 2026
Feyn API
Everything the website shows, as JSON: the verdict, every scam (S) and malicious (M) finding, and the wallets and transactions behind each one. Request a report over HTTPS, or subscribe over a WebSocket and get it live.
- Base URL
https://api.feyn.fun- Auth
- API key (
Authorization: Bearer) or an x402 payment - Format
- JSON, schema
feyn.report/1 - Live
wss://api.feyn.fun/v1/ws, free
Payments open at the $FEYN launch.
Until then /v1/token/{mint} and its summary are free, within the per-IP rate limits, and need no key or payment. The endpoints marked Available at launch answer 404 until then. Everything below is the API as it runs from the launch.
Getting started
Quickstart
- Get a key. Buy a credit pack on the pricing page with one payment from your Solana wallet. The starter pack is $25 for 6,000 credits. The key is shown once; keep it in an environment variable.
- Ask for a token. One report costs 1 credit. Every response says how many are left in
X-Feyn-Credits-Remaining. - Read the verdict.
token.verdict.levelisscam(an S-class finding fired),high_riskorcaution(M-class findings only; the site shows these as “Warning · high risk” and “Warning”, since an M class firing does not make a token malicious),clean(nothing fired: no flags, which is not a promise) orinsufficient_data.
curl https://api.feyn.fun/v1/token/B4HpTokrHXwYGQUYCN1axBH54pYf483Bpw5sCft2pump \
-H "Authorization: Bearer $FEYN_API_KEY"const res = await fetch("https://api.feyn.fun/v1/token/B4HpTokrHXwYGQUYCN1axBH54pYf483Bpw5sCft2pump", {
headers: { Authorization: `Bearer ${process.env.FEYN_API_KEY}` },
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.msg}`);
}
const report = await res.json();
console.log(report.token.verdict.level, report.token.verdict.headline);
console.log("credits left:", res.headers.get("X-Feyn-Credits-Remaining"));import os
import requests
res = requests.get(
"https://api.feyn.fun/v1/token/B4HpTokrHXwYGQUYCN1axBH54pYf483Bpw5sCft2pump",
headers={"Authorization": f"Bearer {os.environ['FEYN_API_KEY']}"},
timeout=10,
)
res.raise_for_status()
report = res.json()
print(report["token"]["verdict"]["level"], report["token"]["verdict"]["headline"])
print("credits left:", res.headers["X-Feyn-Credits-Remaining"])Until the $FEYN launch, leave the key out: the same request is free, within the per-IP rate limits. From the launch it answers 402 without one.
Authentication
Send your API key as a bearer token. Keys start with feyn_sk_. Feyn stores only a hash of each key, so keep yours out of client-side code and logs.
GET /v1/token/B4HpTokrHXwYGQUYCN1axBH54pYf483Bpw5sCft2pump HTTP/1.1
Host: api.feyn.fun
Authorization: Bearer feyn_sk_…Every keyed response carries two headers:
| Header | Meaning |
|---|---|
X-Feyn-Credits-Remaining | Credits left on the key after this call; unlimited on an Enterprise key. |
X-Feyn-Key-Expires | When the key expires, in unix seconds. |
X-Request-Id | On every response, keyed or not. Quote it when something goes wrong. |
What is free: a bad address (400) or an unknown token (404) is answered before any charge. A 304 revalidation is refunded. If the server fails after charging (5xx), the credits come back.
The website, the Telegram bot, the WebSocket, /v1/resolve, /v1/blob and /healthz need no key.
Paying with x402
Available at launch Payments open at the $FEYN launch.
Payments use x402 v2 with the Solana exact scheme: a 402 answer carries a quote, you pay it with one token transfer, and repeat the request with the payment attached. Feyn runs its own facilitator and pays the network fee. Pay in USDC at list price, or in $FEYN at 15% off. SOL is not accepted.
Single call
Without a key, GET /v1/token/{mint} costs $0.01 per report, paid on the request itself. Paid attempts are limited to 2 per second per IP.
- Client to Feyn API: GET /v1/token/{mint}, no key
- Feyn API to Client: 402 with PAYMENT-REQUIRED: one quote per asset, each with extra.memo
- Client: Build the transfer, sign as payer only
- Client to Feyn API: Same request + PAYMENT-SIGNATURE
- Feyn API to Solana: Co-sign as fee payer, simulate, broadcast
- Solana to Feyn API: Confirmed
- Feyn API to Client: 200 with the report + PAYMENT-RESPONSE
The transaction must have exactly this shape, or it is refused before broadcast:
| # | Instruction | Rule |
|---|---|---|
| 0 | ComputeBudget SetComputeUnitLimit | No accounts, at most 100,000 units. |
| 1 | ComputeBudget SetComputeUnitPrice | No accounts, at most 50,000 micro-lamports per unit. |
| 2 | TransferChecked (SPL Token or Token-2022) | The exact amount of the quoted asset to the associated token account of payTo, signed by the payer. |
| 3 | Memo | Exactly extra.memo: the quote, signed by the server. |
Two signers only (the fee payer extra.feePayer at index 0, then you), no address lookup tables, no extra instructions. Wallets that inject Lighthouse assertions are refused.
{
"x402Version": 2,
"error": "payment required",
"resource": {
"url": "https://api.feyn.fun/v1/token/B4HpTokrHXwYGQUYCN1axBH54pYf483Bpw5sCft2pump",
"description": "Feyn token report",
"mimeType": "application/json"
},
"accepts": [
{
"scheme": "exact",
"network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
"amount": "10000",
"asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"payTo": "<vault owner>",
"maxTimeoutSeconds": 300,
"extra": {
"feePayer": "<facilitator>",
"memo": "feyn1:c:usdc:10000:<exp>:<nonce>:<mac>",
"name": "USDC",
"decimals": 6
}
},
{
"…": "the same for $FEYN at 15% off (maxTimeoutSeconds ≤ 60), while a TWAP quote exists"
}
]
}# 1. The quote
curl -si https://api.feyn.fun/v1/token/B4HpTokrHXwYGQUYCN1axBH54pYf483Bpw5sCft2pump | grep -i payment-required
# 2. Sign the transfer with an x402 v2 client, then:
curl https://api.feyn.fun/v1/token/B4HpTokrHXwYGQUYCN1axBH54pYf483Bpw5sCft2pump \
-H "PAYMENT-SIGNATURE: $PAYMENT_B64"const url = "https://api.feyn.fun/v1/token/B4HpTokrHXwYGQUYCN1axBH54pYf483Bpw5sCft2pump";
// 1. The quote: a 402 whose PAYMENT-REQUIRED header lists one option per asset.
const quote = await fetch(url);
const required = JSON.parse(atob(quote.headers.get("PAYMENT-REQUIRED")));
const usdc = required.accepts[0];
// 2. Build and sign the transfer (any x402 v2 client for Solana "exact" does this).
const payload = await x402Client.createPayment(usdc);
// 3. The same request, paid.
const res = await fetch(url, {
headers: { "PAYMENT-SIGNATURE": btoa(JSON.stringify(payload)) },
});
const report = await res.json();import base64
import json
import requests
url = "https://api.feyn.fun/v1/token/B4HpTokrHXwYGQUYCN1axBH54pYf483Bpw5sCft2pump"
# 1. The quote
quote = requests.get(url, timeout=10)
required = json.loads(base64.b64decode(quote.headers["PAYMENT-REQUIRED"]))
usdc = required["accepts"][0]
# 2. Build and sign the transfer with an x402 v2 client
payload = x402_client.create_payment(usdc)
# 3. The same request, paid
res = requests.get(url, headers={"PAYMENT-SIGNATURE": base64.b64encode(json.dumps(payload).encode()).decode()}, timeout=60)
report = res.json()Credit packs
A pack is bought the same way, from POST /v1/keys?pack=…: the paid request answers 201 with the key. A payment buys exactly one key; a replayed payment is refused. The key becomes active once its payment is final (usually within seconds; otherwise the answer is 202 and the key activates on first use).
- Client to Feyn API: POST /v1/keys?pack=starter
- Feyn API to Client: 402 with the pack's quotes
- Client: Build the transfer, sign as payer only
- Client to Feyn API: Same request + PAYMENT-SIGNATURE
- Feyn API to Solana: Co-sign, broadcast, wait until final
- Feyn API to Client: 201 with the key, shown once
- Client to Feyn API: Every later call: Authorization: Bearer feyn_sk_…
USDC quotes hold for 300 seconds and $FEYN quotes for 60. The $FEYN price is a 10-minute average of its own trades; no $FEYN quote is offered while that price is unreliable, and USDC is always offered. On the website, the pricing page does all of this with your wallet.
Plans and pricing
Every call with a pack's key spends credits; Enterprise calls spend none. Prices and costs below come from the same configuration the API server loads. $FEYN prices are 15% below the USDC price.
| Call | Credits |
|---|---|
GET /v1/token/{mint}, the full report | 1 |
GET /v1/token/{mint}/summary | 1 |
| Evidence or wallet-graph export | 2 |
| Batch lookup, per token | 1 |
/v1/keys/self, /v1/pricing, /v1/resolve, /v1/blob, /healthz | 0 |
| Plan | USDC | In $FEYN | Credits | Key lasts | Requests/s |
|---|---|---|---|---|---|
| Starter | $25 | $21.25 | 6,000 | 30 days | 10 |
| Builder | $100 | $85 | 30,000 | 60 days | 20 |
| Pro | $500 | $425 | 200,000 | 90 days | 200 |
| Enterprise, with the firehose | $1,500/month | $1,275/month | Unlimited lookups | 30 days, renewable | 2,000 |
| Single call (x402, no key) | $0.01 | $0.0085 | 1 report | — | 2 per IP |
A key's lifetime counts from its payment. Credits left at expiry expire with it. Every plan gets the same reports; Enterprise adds the firehose, 24/7 support and monthly renewal of the same key.
Rate limits
Each key has its own limit, set by its pack. Over it, the answer is 429 rate_limited with Retry-After (seconds) and retry_after_s in the body.
| Who | Requests/s | Burst |
|---|---|---|
| Starter key | 10 | 20 |
| Builder key | 20 | 40 |
| Pro key | 200 | 400 |
| Enterprise key | 2000 | 4000 |
| x402 paid attempts, per IP | 2 | 4 |
| Free lookups (no key, paid mode off), per IP | 30/min | 15 |
Payments have two more limits that protect the fee payer. A network (IPv4 /24, IPv6 /48) whose payments failed on chain and used its share of the fee budget gets 429 network_throttled for 5 minutes. Under a drain attempt new payers settle one at a time, and a payment that waits over 10 s gets 503 settlement_busy. API keys are never affected by either.
Errors
Every error has the same JSON body. Switch on code, never on msg. retry_after_s (and the Retry-After header) appear when waiting helps.
{
"error": {
"code": "key_expired",
"msg": "key expired",
"request_id": "01J9…",
"retry_after_s": 5
}
}| Status | Code | When | What to do |
|---|---|---|---|
| 400 | bad_request | A bad query parameter, blob hash or resolve query. | Fix the request. Nothing was charged. |
| 400 | invalid_address | The path address is not base58 or not 32 bytes (64 bytes is a signature). | Fix the address. Nothing was charged. |
| 400 | invalid_payment | PAYMENT-SIGNATURE is not base64 JSON of a PaymentPayload. | Fix the encoding. Nothing was charged. |
| 401 | invalid_key | The key is malformed, unknown or revoked, or its payment failed or never landed. | Check the Authorization header, or buy a new pack. |
| 401 | key_pending | The key's payment is not final yet. | Retry after Retry-After seconds. |
| 401 | key_expired | The key is past expires_at. | Buy a new pack. Unused credits expire with the key. |
| 401 | invalid_signature | Key recovery: the message was not signed by the wallet that paid. | Sign with the paying wallet. |
| 402 | credits_exhausted | The key has no credits left. | Buy a new pack. |
| 403 | plan_required | The firehose (GET /v1/firehose) with a key whose plan has no firehose. | Use an Enterprise key. |
| 403 | bot_blocked | A free request (no key, no payment) from a client detected as automated: sustained, machine-like traffic that does not come from feyn.fun. | Use an API key or pay per call with x402. The block lifts after Retry-After seconds once the traffic stops. |
| 403 | payer_blocked | This payer, source account or IP had 3 payments fail on chain within 24 h. | Wait 24 h, or use an API key. |
| 404 | not_a_mint | The address is a curve, pool, wallet or other account; the body's resolved names its mint. | Follow resolved.mint. Nothing was charged. |
| 404 | not_indexed | A pump.fun token created before Feyn started indexing, with no activity since (no backfill). | Nothing to fetch until it trades again. Nothing was charged. |
| 404 | not_found | Unknown resource, or the token is not one Feyn tracks. | Check the path. Nothing was charged. |
| 404 | unknown_ref | A blob hash that is not stored. | Re-read the report for current refs. Nothing was charged. |
| 405 | method_not_allowed | A method the endpoint does not take (GET, HEAD and OPTIONS everywhere; POST only on /v1/keys). | Use the documented method. |
| 409 | payment_in_flight | Another payment from this payer or source account is not settled yet. | Retry after Retry-After (5 s). |
| 409 | already_used | Key recovery: this signed message was already used. | Sign a new message with a fresh timestamp. |
| 429 | too_many_sockets | The key already has 5 firehose sockets open. | Close one, then reconnect. |
| 429 | rate_limited | Over the key's tier, the per-IP paid-attempt limit or the free lookup budget. | Back off for Retry-After seconds. |
| 429 | network_throttled | Payments from your client network (IPv4 /24, IPv6 /48) failed on chain and used its share of the fee budget. | Retry after Retry-After (300 s), or use an API key. |
| 500 | internal | An unexpected failure. Credits taken for the call are refunded. | Retry once, then report the X-Request-Id. |
| 503 | settlement_pending | A single-call payment was broadcast but has not confirmed in time. | Check PAYMENT-RESPONSE.transaction, then re-send the same PAYMENT-SIGNATURE rather than paying again. |
| 503 | settlement_paused | The fee-payer spend budget for your payer class (or overall) is used up. | Pay later (Retry-After, 60 s) or use an API key. |
| 503 | settlement_busy | New payers settle one at a time during a drain attempt, and your turn did not come within 10 s. | Retry shortly (Retry-After, 2 s). |
| 503 | unavailable | Overload, warm-up, a standby server, or the key store, payment store or RPC is down. | Retry after Retry-After seconds. |
A refused payment answers 402 with fresh quotes and a PAYMENT-RESPONSE whose errorReason says why:
invalid_payment_requirements- The quote is unknown, altered or expired. Ask for a fresh 402 and pay again.
duplicate_settlement- This payment was already used. One payment buys exactly one key or call.
insufficient_funds- The paying wallet does not hold enough of the asset.
invalid_transaction_state- The payment transaction failed on chain.
invalid_exact_svm_payload_transaction_*- The transaction is not exactly the accepted shape (compute budget, one TransferChecked, the memo).
Reference
Tokens
GET/v1/token/{mint}
The full report (feyn.report/1) for an indexed pump.fun token: the verdict, every finding with its evidence tables and graphs, holders and context. 1 credit, or $0.01 with x402.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
mintrequired | path | base58 | The token's mint address (32 bytes). Case-sensitive. |
sections | query | list | Comma list of summary, scam, malicious, context, tables, graphs. Default: all. Fields not selected are left out; schema, mint, rev, slot, updated_at, coverage, addrs, labels, hashes are always there. |
inline | query | list | Comma list of sigs, graphs, tables: include those lazy chunks in a top-level chunks map keyed by ref (up to 8 MB; chunks_truncated when capped). Otherwise fetch them from /v1/blob. |
Authorization | header | string | Bearer API key. Or send PAYMENT-SIGNATURE instead. |
If-None-Match | header | etag | The ETag you hold (the report's rev). A match answers 304, not charged. |
Responses
- 200
- The report.
ETagandX-Feyn-Revhold its revision. - 304
- Unchanged since If-None-Match.
- 400
invalid_addressorbad_request. Free.- 402
- Payment required (x402 quotes), or
credits_exhausted. - 404
not_a_mint(the body'sresolvednames the token),not_indexedornot_found. Free.- 429
rate_limitedornetwork_throttled.- 503
unavailable, or a payment state:settlement_pending,settlement_paused,settlement_busy.
curl https://api.feyn.fun/v1/token/B4HpTokrHXwYGQUYCN1axBH54pYf483Bpw5sCft2pump?sections=summary,malicious \
-H "Authorization: Bearer $FEYN_API_KEY"const res = await fetch("https://api.feyn.fun/v1/token/B4HpTokrHXwYGQUYCN1axBH54pYf483Bpw5sCft2pump?sections=summary,malicious", {
headers: { Authorization: `Bearer ${process.env.FEYN_API_KEY}` },
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.msg}`);
}
console.log(await res.json());import os
import requests
res = requests.get(
"https://api.feyn.fun/v1/token/B4HpTokrHXwYGQUYCN1axBH54pYf483Bpw5sCft2pump?sections=summary,malicious",
headers={"Authorization": f"Bearer {os.environ['FEYN_API_KEY']}"},
timeout=10,
)
res.raise_for_status()
print(res.json()){
"schema": "feyn.report/1",
"mint": "B4HpTokrHXwYGQUYCN1axBH54pYf483Bpw5sCft2pump",
"rev": 450641306003,
"slot": 450641306,
"updated_at": 1790416804,
"coverage": {
"indexed_since_slot": 450300000,
"complete": true
},
"addrs": [
"D9gQ6RhKEpnobPBUdWY5bPQt2p3zGk3iVz6ChpUi2ArA",
"tExRzb1XFwHP9A5jnZyJTfDTpFtRvNSn3FhGoVY1GuB"
],
"labels": [
{
"a": 0,
"kind": "creator"
},
{
"a": 1,
"kind": "curve"
}
],
"token": {
"name": "Example Small Curve",
"symbol": "SMOL",
"creator": 0,
"created_slot": 450636806,
"created_at": 1790415600,
"venue": "curve",
"quote_mint": "So11111111111111111111111111111111111111112",
"curve": {
"address": 1,
"progress_bps": 1661,
"real_quote_reserves": 4200000000,
"complete": false,
"mayhem_mode": false
},
"price_quote": 3.6335507921714823e-8,
"price_usd": 0.00000545,
"mcap_usd": 5450,
"liquidity_usd": 630,
"supply_raw": 1000000000000000,
"decimals": 6,
"holders": {
"total": 14,
"fake_dust": 0,
"top10_bps": 1873,
"top10_true_bps": 1873
},
"launch_block_bps": 346,
"trades_24h": {
"buys": 21,
"sells": 7,
"volume_quote": 5900000000,
"organic_volume_bps": 10000
},
"traders": {
"total": 16,
"fresh_bps": 0,
"flagged_bps": 0
},
"verdict": {
"scam": false,
"level": "caution",
"scam_ids": [],
"malicious_ids": [
"M004"
],
"headline": "No scam detected. Malicious behaviour found: Block-0 buyers."
},
"score": {
"value": 0,
"band": "no_red_flags",
"version": "598826f044206d43",
"why": []
},
"insiders": {
"wallets": 1,
"at_least": false,
"bought_bps": 346,
"bought_market_bps": 346,
"received_bps": 0,
"sold_bps": 0,
"sent_bps": 0,
"held_bps": 346,
"held_of_bought_bps": 10000,
"launch_block_of_bought_bps": 10000,
"net_bought_bps": 346,
"net_sold_bps": 0,
"round_trip_bps": 0,
"held_of_net_bought_bps": 10000,
"bought_raw": 34612903225806,
"sold_raw": 0,
"held_raw": 34612903225806,
"buy_quote": 1000000000,
"sell_quote": 0,
"realised_quote": 0,
"realised_usd": 0,
"held_value_quote": 1257677419,
"held_value_usd": 188.65,
"wash_netted": false,
"groups": [
{
"group": "creator",
"wallets": 1,
"bought_bps": 346,
"sold_bps": 0,
"held_bps": 346,
"net_bought_bps": 346,
"net_sold_bps": 0,
"received_bps": 0,
"sent_bps": 0,
"realised_quote": 0
}
]
}
},
"malicious_behaviour": [
{
"name": "Block-0 buyers",
"headline": "3.46% of the supply was bought in the same block the token launched, by the creator alone.",
"explanation": "The creator bought in the same block the token was created, before anyone else could see it. The creator spent 1 SOL. The creator still holds all of it. The table shows exactly who bought, how much and in what order.\n\nWhy it matters: A creator's launch-block buy is visible to everyone and is common; it matters if the creator sells into later buyers.",
"severity": 2,
"confidence": "standard",
"first_detected_slot": 450636806,
"updated_slot": 450636806,
"wallets": [
0
],
"examples": [],
"tables": [
"block0_buyers"
],
"id": "M004",
"metrics": {
"create_slot": 450636806,
"create_ix": 212,
"buyers": 1,
"supply_bps": 346,
"tokens_raw": 34612903225806,
"quote_spent": 1000000000,
"tips_lamports": 0,
"creator_included": true,
"creator_supply_bps": 346,
"largest_non_creator_bps": 0,
"linked_to_creator": 0,
"held_now_bps": 346,
"true_held_bps": 346
}
}
],
"hashes": {
"addrs": "9bc01ae70874bc54",
"malicious/M004": "d9b2b17dca920c51",
"summary": "12ef3c252b3985d2"
}
}GET/v1/token/{mint}/summary
The verdict, the headline of each finding and the key figures: exactly the WebSocket's summary frame. Small and fast; the Telegram bot renders from it. 1 credit.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
mintrequired | path | base58 | The token's mint address. |
Same statuses, ETag and payment rules as the full report. No query parameters.
curl https://api.feyn.fun/v1/token/B4HpTokrHXwYGQUYCN1axBH54pYf483Bpw5sCft2pump/summary \
-H "Authorization: Bearer $FEYN_API_KEY"const res = await fetch("https://api.feyn.fun/v1/token/B4HpTokrHXwYGQUYCN1axBH54pYf483Bpw5sCft2pump/summary", {
headers: { Authorization: `Bearer ${process.env.FEYN_API_KEY}` },
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.msg}`);
}
const { token, findings } = await res.json();
console.log(token.verdict.level, findings.map((f) => f.id));import os
import requests
res = requests.get(
"https://api.feyn.fun/v1/token/B4HpTokrHXwYGQUYCN1axBH54pYf483Bpw5sCft2pump/summary",
headers={"Authorization": f"Bearer {os.environ['FEYN_API_KEY']}"},
timeout=10,
)
res.raise_for_status()
s = res.json()
print(s["token"]["verdict"]["level"], [f["id"] for f in s["findings"]]){
"schema": "feyn.report/1",
"mint": "B4HpTokrHXwYGQUYCN1axBH54pYf483Bpw5sCft2pump",
"rev": 450641306003,
"slot": 450641306,
"updated_at": 1790416804,
"coverage": {
"indexed_since_slot": 450300000,
"complete": true
},
"addrs": [
"D9gQ6RhKEpnobPBUdWY5bPQt2p3zGk3iVz6ChpUi2ArA",
"tExRzb1XFwHP9A5jnZyJTfDTpFtRvNSn3FhGoVY1GuB"
],
"token": {
"name": "Example Small Curve",
"symbol": "SMOL",
"creator": 0,
"created_slot": 450636806,
"created_at": 1790415600,
"venue": "curve",
"quote_mint": "So11111111111111111111111111111111111111112",
"curve": {
"address": 1,
"progress_bps": 1661,
"real_quote_reserves": 4200000000,
"complete": false,
"mayhem_mode": false
},
"price_quote": 3.6335507921714823e-8,
"price_usd": 0.00000545,
"mcap_usd": 5450,
"liquidity_usd": 630,
"supply_raw": 1000000000000000,
"decimals": 6,
"holders": {
"total": 14,
"fake_dust": 0,
"top10_bps": 1873,
"top10_true_bps": 1873
},
"launch_block_bps": 346,
"trades_24h": {
"buys": 21,
"sells": 7,
"volume_quote": 5900000000,
"organic_volume_bps": 10000
},
"traders": {
"total": 16,
"fresh_bps": 0,
"flagged_bps": 0
},
"verdict": {
"scam": false,
"level": "caution",
"scam_ids": [],
"malicious_ids": [
"M004"
],
"headline": "No scam detected. Malicious behaviour found: Block-0 buyers."
},
"score": {
"value": 0,
"band": "no_red_flags",
"version": "598826f044206d43",
"why": []
},
"insiders": {
"wallets": 1,
"at_least": false,
"bought_bps": 346,
"bought_market_bps": 346,
"received_bps": 0,
"sold_bps": 0,
"sent_bps": 0,
"held_bps": 346,
"held_of_bought_bps": 10000,
"launch_block_of_bought_bps": 10000,
"net_bought_bps": 346,
"net_sold_bps": 0,
"round_trip_bps": 0,
"held_of_net_bought_bps": 10000,
"bought_raw": 34612903225806,
"sold_raw": 0,
"held_raw": 34612903225806,
"buy_quote": 1000000000,
"sell_quote": 0,
"realised_quote": 0,
"realised_usd": 0,
"held_value_quote": 1257677419,
"held_value_usd": 188.65,
"wash_netted": false,
"groups": [
{
"group": "creator",
"wallets": 1,
"bought_bps": 346,
"sold_bps": 0,
"held_bps": 346,
"net_bought_bps": 346,
"net_sold_bps": 0,
"received_bps": 0,
"sent_bps": 0,
"realised_quote": 0
}
]
}
},
"findings": [
{
"id": "M004",
"name": "Block-0 buyers",
"headline": "3.46% of the supply was bought in the same block the token launched, by the creator alone.",
"severity": 2
}
]
}The insiders object
token.insiders, in the report and the summary: every wallet a shown scam or malicious finding names (the creator, launch-block buyers, linked clusters and hubs, snipers, wash traders and bots), each counted once, and what that set did with the token. Shares are basis points oftoken.supply_raw. The object is absent when no finding names a wallet. It updates live, with every trade of the token.
- Tokens moving between insiders cancel out. What reached the set from outside by transfer counts as
received; what left it without a sale (sent to another wallet, or burned) counts assent, not as sold. - Transfers are followed: a wallet that received the token from an insider outside a trade (and, a few hops on, its own recipients) counts as an insider of the sender's group. What it holds is in
held_bps, what it sells insold_bpsand the profit, and the transfer is notsent.followed_walletssays how many such wallets are counted. Exchanges, routers, programs and dust recipients are never followed: tokens sent there staysent. - Wash traders (M001 to M003 wash wallets, M010 bots) are counted net: tokens they bought and sold back add to neither
boughtnorsold. Their SOL stays gross inbuy_quoteandsell_quote, since a round trip costs real fees. - Flagged traders (M011) are not insiders: their record is on other tokens.
| Field | Type | Meaning |
|---|---|---|
wallets | integer | Insider wallets counted (programs, pools, vaults and protocol actors are never counted). |
at_least | boolean | The set may be incomplete (an operator cluster truncated by its wallet cap, a finding that counts more wallets than it lists, history from before indexing): every figure is a lower bound. |
unknown_wallets | integer? | Insider wallets (counted in wallets) whose trades on the token Feyn no longer holds: an idle token's trade history is dropped after 2 hours and restarts on its next trade. Their figures are left out and at_least is set. |
figures_unavailable | boolean? | No figure is known (every insider's history is gone). The shares and amounts are then not measurements: show “unavailable”, never 0%. |
bought_bps | bps | Supply the insiders acquired, gross: bought_market_bps + received_bps. Counts every buy, so a wallet trading in and out counts each time; capped at 100% (see net_bought_bps). |
bought_market_bps | bps | Supply they bought on the bonding curve and PumpSwap pools, the creator's launch buy included. |
received_bps | bps | Supply that reached them by transfer from wallets outside the set (net). |
sold_bps | bps | Supply they sold on the curve and pools, gross: every sell counts, capped at 100% (see net_sold_bps). |
sent_bps | bps | Supply that left them other than by a sale: sent to wallets outside the set, or burned (net). |
held_bps | bps | Supply they hold now. bought − sold − sent = held. |
held_of_bought_bps | bps | Share of what they acquired that they still hold. |
launch_block_of_bought_bps | bps? | Share of their market buys made in the launch block. Absent when the launch predates indexing. |
net_bought_bps | bps | Supply they took from the market and did not sell back: each wallet's buys minus its own round trips, plus received_bps. At most 100%. Use this, not bought_bps, for “how much they bought”. |
net_sold_bps | bps | Supply they sold beyond what the same wallet bought (tokens from the launch or from transfers, e.g. a consolidating hub). net_bought − net_sold − sent = held. |
round_trip_bps | integer | Supply bought and sold back by the same wallet. Not capped: 25000 means they traded the supply 2.5 times over. |
held_of_net_bought_bps | bps | Share of net_bought they still hold. |
bought_raw, sold_raw, held_raw | integer | The same amounts in raw token units. |
buy_quote, sell_quote | integer | Quote paid for every buy and received for every sell, in base units of token.quote_mint (lamports for SOL; for another quote mint divide by 10^token.quote_decimals and label with token.quote_symbol, and token.quote_usd is the USD value of one whole quote token when Feyn has a reliable price). Curve trades are counted before pump.fun's fee, PumpSwap trades with it. |
realised_quote | integer | Realised profit on what they sold: per role group, its sells minus the cost of the tokens it sold at its average cost (its buys over every token it acquired; tokens that reached it by transfer count at no cost), summed. Tokens still held or sent away keep their cost. Negative when they sold below cost. |
realised_usd | number? | realised_quote in USD at the current rate. Absent without a USD rate for the quote unit. |
held_value_quote, held_value_usd | number? | What they still hold at the current price. Unrealised: selling it would move the price. |
followed_wallets, followed_held_bps, followed_sold_bps | number? | Wallets counted because an insider sent them the token, and the supply they hold and sold (parts of held_bps and sold_bps). Absent when there are none. |
wash_netted | boolean | Wash trading was counted net (see below). |
groups[] | array | By role, each wallet in one group only: group (creator, dev_team, launch_bundle, cluster, sniper, wash_bot, other), wallets, bought_bps, sold_bps (gross), net_bought_bps, net_sold_bps, received_bps, sent_bps (transfers with any wallet outside the group, other insiders included; so each group adds up: net_bought + received − net_sold − sent = held), held_bps, realised_quote. |
The outcomes object
token.outcomes, in the report and the summary: what happened to earlier launches that looked like this token at the same age. Historical, not a prediction and not advice. It is computed from a versioned table (table) of Feyn's own history, for pump.fun launches Feyn saw from creation, on their curve and not in Mayhem mode; other tokens have no outcomes.
at_secsis the comparison point (120, 300, 900 or 3600 seconds after launch): the latest one the token had reached. Its signals and trading activity are compared with other launches' at that age. After 90 minutes the last object stays as it was.activity:0= no trade in the preceding minute;1–5= quintile of the SOL bought so far among launches still trading (5 = most). Every rate is within this bucket.rows: one per qualifying signal the token has (signal: a class id such asS006, orM021_copy,launch_block_30,top10_25,creator_5,creator_serial,creator_sold), withlabel, and the rates of comparable launcheswithandwithoutit.similaris every comparable launch.- Rates are in basis points of the group:
crashed_80_1h_bps(price fell to 20% or less within the next hour),doubled_6h_bps(reached 2× within 6 hours),graduated_24h_bps(curve completed within 24 hours of launch).n/n_24hare the group sizes; a rate is absent when its group is undermin_n.
The clusters object
token.clusters, in the report and the summary: wallets on this token (its insiders and its creator) that belong to a wallet cluster, a group of wallets that trade as one across many launches. Absent when none does. At most 3 clusters, highest confidence first.
- Clusters are found from trading behaviour over the last few days of pump.fun launches: the same launches bought within a few blocks of each other, correlated activity, identical buy sizes. Not from funding or signer links: a cluster says the wallets act as one group (one operator or a coordinated group), not who controls them. Recomputed every hour; only clusters with
confidenceof 60 or more and same-block evidence are listed. confidence(0–100) is calibrated: of clusters rated 70, about 70% were confirmed by an independent on-chain link check.members[]: the cluster's wallets on this token (at most 12;members_morecounts the rest), eacha(an index intoaddrs; the summary's ownaddrscovers only the creator, curve and pool, so resolve it in the report's or theaddrssection's table) and its insiderrole(as ininsiders.groups;creatorfor the creator).creatoris set when the creator is a member;insiderscounts the token's insiders it covers;sizeis the whole cluster.- Cluster-wide, over the computation's window (
window_hours,generation):together(launches two of its wallets bought within a few blocks of each other),same_block_bpsandidentical_bps(of those co-buys, the share in the same block / with identical sizes),early_bps(first buys made right after launch),overlap_bps(overlap of the launches they bought),activity_corr_bps(correlation of their activity over time),onchain_link(an on-chain link also joins two of them),staggered_bps(optional: of the launches two of its wallets bought, the share bought one after another, more than a minute and at most ten minutes apart).
GET/v1/resolve/{q}
What an address or link is: a mint, its bonding curve or pool, a creator wallet, or not a token. Use it to turn a curve, pool or pump.fun link into the mint to ask for. Free.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
qrequired | path | string | An address, or a percent-encoded pump.fun/coin/…, solscan.io/token/… or dexscreener.com/solana/… link. Up to 256 characters. |
kind is one of mint, curve, pool, creator (with up to 20 tokens), wallet, unknown_token, unsupported_launchpad (a Raydium LaunchLab token, with platform and launchpad), not_a_token, invalid (with a reason) or not_indexed. Every well-formed request answers 200; the answer is the kind, not the status.
curl https://api.feyn.fun/v1/resolve/tExRzb1XFwHP9A5jnZyJTfDTpFtRvNSn3FhGoVY1GuBconst res = await fetch("https://api.feyn.fun/v1/resolve/tExRzb1XFwHP9A5jnZyJTfDTpFtRvNSn3FhGoVY1GuB");
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.msg}`);
}
console.log(await res.json());import requests
res = requests.get(
"https://api.feyn.fun/v1/resolve/tExRzb1XFwHP9A5jnZyJTfDTpFtRvNSn3FhGoVY1GuB",
timeout=10,
)
res.raise_for_status()
print(res.json()){
"q": "tExRzb1XFwHP9A5jnZyJTfDTpFtRvNSn3FhGoVY1GuB",
"kind": "curve",
"mint": "B4HpTokrHXwYGQUYCN1axBH54pYf483Bpw5sCft2pump",
"name": "Example Small Curve",
"symbol": "SMOL"
}GET/v1/blob/{hash}
A lazy chunk of a report: a page of signatures, a table page or a subgraph, by the ref a report points at. Immutable and cached for a year. Free.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
hashrequired | path | hex(16) | A Ref.ref from the report (Finding.txs, Table.next, GraphManifest.expand, …). |
X-Feyn-Chunk says what it is: sigs, table or graph. A ref is kept for at least 24 hours after the last report that uses it.
curl https://api.feyn.fun/v1/blob/d550411a3d83bf4aconst res = await fetch("https://api.feyn.fun/v1/blob/d550411a3d83bf4a");
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.msg}`);
}
console.log(await res.json());import requests
res = requests.get(
"https://api.feyn.fun/v1/blob/d550411a3d83bf4a",
timeout=10,
)
res.raise_for_status()
print(res.json())Reference
Keys and payments
POST/v1/keys?pack={name}Available at launch
Buy a credit pack. Without payment the answer is a 402 quote for the pack; with PAYMENT-SIGNATURE it is the key.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
packrequired | query | string | starter, builder, pro, enterprise. |
PAYMENT-SIGNATURE | header | base64 | base64 JSON of the x402 PaymentPayload for one of the quotes. |
Responses
- 201
- The key, shown once. Feyn stores only its SHA-256.
- 202
status: "pending": the payment was broadcast but is not final yet; the key activates on first use once it is.- 402
- The quote (starter: 25000000 USDC atoms), or a refused payment with
errorReason. - 403 · 409 · 429 · 503
payer_blocked,payment_in_flight,network_throttled,settlement_paused/settlement_busy.
# The quote for the starter pack
curl -si -X POST "https://api.feyn.fun/v1/keys?pack=starter"
# Paid (see Paying with x402)
curl -X POST "https://api.feyn.fun/v1/keys?pack=starter" \
-H "PAYMENT-SIGNATURE: $PAYMENT_B64"const url = "https://api.feyn.fun/v1/keys?pack=starter";
const quote = await fetch(url, { method: "POST" }); // 402
const required = JSON.parse(atob(quote.headers.get("PAYMENT-REQUIRED")));
const payload = await x402Client.createPayment(required.accepts[0]);
const res = await fetch(url, {
method: "POST",
headers: { "PAYMENT-SIGNATURE": btoa(JSON.stringify(payload)) },
});
const { key, credits, expires_at } = await res.json(); // shown onceimport base64
import json
import requests
url = "https://api.feyn.fun/v1/keys?pack=starter"
quote = requests.post(url, timeout=10) # 402
required = json.loads(base64.b64decode(quote.headers["PAYMENT-REQUIRED"]))
payload = x402_client.create_payment(required["accepts"][0])
res = requests.post(url, headers={"PAYMENT-SIGNATURE": base64.b64encode(json.dumps(payload).encode()).decode()}, timeout=60)
key = res.json()["key"] # shown once{
"key": "feyn_sk_…",
"status": "active",
"pack": "starter",
"credits": 6000,
"rate_limit": {
"per_s": 10,
"burst": 20
},
"lifetime_days": 30,
"expires_at": 1793008800,
"transaction": "<signature>"
}POST/v1/keys/recoverAvailable at launch
Lost the key? The wallet that paid for it signs a message and gets a new secret for the same key (same credits and expiry). The old secret stops working.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
transactionrequired | body | base58 | The signature of the transaction that paid for the key. |
timestamprequired | body | unix s | Now; must be within 5 minutes of the server's clock. |
signaturerequired | body | base58 | The paying wallet's signMessage over feyn-key-recovery:v1:<network>:<transaction>:<timestamp> (UTF-8). |
Responses
- 201
- A new secret for the same key.
- 401
invalid_signature: signed by another wallet.- 404
- The transaction paid for a single call, or failed.
- 409
already_used: each signed message works once.
curl -X POST https://api.feyn.fun/v1/keys/recover \
-H "Content-Type: application/json" \
-d '{"transaction":"<base58>","timestamp":1790416804,"signature":"<base58>"}'const timestamp = Math.floor(Date.now() / 1000);
const message = `feyn-key-recovery:v1:solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:${transaction}:${timestamp}`;
const signature = await wallet.signMessage(new TextEncoder().encode(message));
const res = await fetch("https://api.feyn.fun/v1/keys/recover", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ transaction, timestamp, signature: bs58.encode(signature) }),
});
const { key } = await res.json(); // the new secretimport time
import requests
timestamp = int(time.time())
message = f"feyn-key-recovery:v1:solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:{transaction}:{timestamp}"
signature = sign_with_paying_wallet(message.encode()) # base58
res = requests.post("https://api.feyn.fun/v1/keys/recover", json={"transaction": transaction, "timestamp": timestamp, "signature": signature}, timeout=10)
key = res.json()["key"]POST/v1/keys?pack=enterpriseAvailable at launch
Renew an Enterprise key: the same request as buying, with the key in Authorization. It adds 30 days to the same key; paid with x402 like a purchase.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
packrequired | query | string | enterprise. |
Authorizationrequired | header | string | Bearer key to renew. Without it, the request buys a new key. |
PAYMENT-SIGNATURE | header | base64 | base64 JSON of the x402 PaymentPayload for one of the quotes. |
Responses
- 200
status: "renewed":added_dayswere added;expires_atis the new expiry.- 202
status: "pending": paid, but not final yet. The days are added once it is; do not pay again.- 402
- "pay to renew this key": the quote ($1,500 USDC, or $FEYN at 15% off), or a refused payment with
errorReason. - 400
bad_request: the key is not Enterprise. Credit packs are not renewed; buy a new one.- 401
invalid_key: unknown or revoked key.
# The renewal quote
curl -si -X POST "https://api.feyn.fun/v1/keys?pack=enterprise" \
-H "Authorization: Bearer $FEYN_API_KEY"
# Paid (see Paying with x402)
curl -X POST "https://api.feyn.fun/v1/keys?pack=enterprise" \
-H "Authorization: Bearer $FEYN_API_KEY" \
-H "PAYMENT-SIGNATURE: $PAYMENT_B64"const url = "https://api.feyn.fun/v1/keys?pack=enterprise";
const auth = { Authorization: `Bearer ${process.env.FEYN_API_KEY}` };
const quote = await fetch(url, { method: "POST", headers: auth }); // 402
const required = JSON.parse(atob(quote.headers.get("PAYMENT-REQUIRED")));
const payload = await x402Client.createPayment(required.accepts[0]);
const res = await fetch(url, {
method: "POST",
headers: { ...auth, "PAYMENT-SIGNATURE": btoa(JSON.stringify(payload)) },
});
const { status, expires_at } = await res.json(); // "renewed"import base64
import json
import os
import requests
url = "https://api.feyn.fun/v1/keys?pack=enterprise"
auth = {"Authorization": f"Bearer {os.environ['FEYN_API_KEY']}"}
quote = requests.post(url, headers=auth, timeout=10) # 402
required = json.loads(base64.b64decode(quote.headers["PAYMENT-REQUIRED"]))
payload = x402_client.create_payment(required["accepts"][0])
sig = base64.b64encode(json.dumps(payload).encode()).decode()
res = requests.post(url, headers={**auth, "PAYMENT-SIGNATURE": sig}, timeout=60)
print(res.json()["status"], res.json().get("expires_at")){
"status": "renewed",
"pack": "enterprise",
"added_days": 30,
"expires_at": 1795600800,
"transaction": "<signature>",
"network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
"asset": "usdc",
"note": "…",
"support_telegram": "@reaperofrugs"
}GET/v1/keys/selfAvailable at launch
Your key's credits, expiry, tier, renewal and paying transaction. Costs no credits (it does count against the tier's rate limit). Also at /v1/keys/me. The key page uses it.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
Authorizationrequired | header | string | Bearer API key. |
expires_at is null while the payment is not final (the lifetime starts from the payment). status is active or pending. On Enterprise, credits_total and credits_left are null (unlimited), and renewal.due turns true 3 days before expiry.
curl https://api.feyn.fun/v1/keys/self \
-H "Authorization: Bearer $FEYN_API_KEY"const res = await fetch("https://api.feyn.fun/v1/keys/self", {
headers: { Authorization: `Bearer ${process.env.FEYN_API_KEY}` },
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.msg}`);
}
console.log(await res.json());import os
import requests
res = requests.get(
"https://api.feyn.fun/v1/keys/self",
headers={"Authorization": f"Bearer {os.environ['FEYN_API_KEY']}"},
timeout=10,
)
res.raise_for_status()
print(res.json()){
"pack": "starter",
"status": "active",
"credits_total": 6000,
"credits_left": 4980,
"expires_at": 1793008800,
"rate_limit": {
"per_s": 10,
"burst": 20
},
"transaction": "<signature>",
"plan": "starter",
"expires_in_s": 2592000,
"unlimited": false,
"firehose": false,
"renewal": {
"renewable": false,
"period_days": null,
"due": false,
"notice_days": 3,
"how": "…"
},
"support_telegram": "@reaperofrugs"
}GET/v1/pricingAvailable at launch
Packs, credit costs, payment assets and the current $FEYN rate. Free; cached for 10 seconds.
No parameters.
curl https://api.feyn.fun/v1/pricingconst res = await fetch("https://api.feyn.fun/v1/pricing");
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.msg}`);
}
console.log(await res.json());import requests
res = requests.get(
"https://api.feyn.fun/v1/pricing",
timeout=10,
)
res.raise_for_status()
print(res.json()){
"x402Version": 2,
"network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
"fee_payer": "<facilitator>",
"single_call": {
"price_usdc": "0.01",
"price_feyn_atoms": "85000000",
"rate_limit_per_ip": {
"per_s": 2,
"burst": 4
}
},
"credit_costs": {
"report": 1,
"summary": 1,
"export": 2,
"batch_per_token": 1
},
"packs": [
{
"name": "starter",
"price_usdc": "25",
"price_feyn_atoms": "212500000000",
"credits": 6000,
"lifetime_days": 30,
"rate_limit": {
"per_s": 10,
"burst": 20
},
"unlimited": false,
"firehose": false,
"renew_days": null
},
{
"name": "builder",
"price_usdc": "100",
"price_feyn_atoms": "850000000000",
"credits": 30000,
"lifetime_days": 60,
"rate_limit": {
"per_s": 20,
"burst": 40
},
"unlimited": false,
"firehose": false,
"renew_days": null
},
{
"name": "pro",
"price_usdc": "500",
"price_feyn_atoms": "4250000000000",
"credits": 200000,
"lifetime_days": 90,
"rate_limit": {
"per_s": 200,
"burst": 400
},
"unlimited": false,
"firehose": false,
"renew_days": null
},
{
"name": "enterprise",
"price_usdc": "1500",
"price_feyn_atoms": "12750000000000",
"credits": null,
"lifetime_days": 30,
"rate_limit": {
"per_s": 2000,
"burst": 4000
},
"unlimited": true,
"firehose": true,
"renew_days": 30
}
],
"support_telegram": "@reaperofrugs",
"assets": {
"usdc": {
"mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"decimals": 6
},
"feyn": {
"mint": "FEYNnky1Jp7xfrTnR5U93qBM79RxYkQibzTaR31fdxqS",
"decimals": 6,
"discount_bps": 1500,
"usdc_per_feyn_twap": 0.0001,
"quote_ttl_s": 60
}
}
}price_feyn_atoms is null while $FEYN has no quote.GET/v1/x402/supportedAvailable at launch
The facilitator's supported schemes and networks (x402 v2 SupportedResponse), for x402 clients that check before paying. Free.
Lists the exact scheme on solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp with the fee payer in extra.feePayer.
curl https://api.feyn.fun/v1/x402/supportedconst res = await fetch("https://api.feyn.fun/v1/x402/supported");
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.msg}`);
}
console.log(await res.json());import requests
res = requests.get(
"https://api.feyn.fun/v1/x402/supported",
timeout=10,
)
res.raise_for_status()
print(res.json())GET/healthz
Liveness and ingest lag, for uptime checks. Not versioned, not rate-limited, never cached.
Responses
- 200
"ok": serving, ingest lag within target."degraded": serving, but reports are behind.- 503
"starting"or"down": not ready.
curl https://api.feyn.fun/healthzconst res = await fetch("https://api.feyn.fun/healthz");
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.msg}`);
}
console.log(await res.json());import requests
res = requests.get(
"https://api.feyn.fun/healthz",
timeout=10,
)
res.raise_for_status()
print(res.json()){
"status": "ok",
"slot": 450641306,
"lag_s": 1.2
}Reference
WebSocket
WSwss://api.feyn.fun/v1/ws
The live feed the website uses: subscribe to a token and get its summary, then each section, then patches as trades land. Free, no auth, rate-limited per IP. The protocol is feyn.ws/1.
Send hello first, then one sub per token (up to 4 per socket; ids strictly increasing). Binary frames carry the data, text frames the control messages. Receivers must ignore unknown fields, message types and section keys: additive changes do not bump the version.
# websocat (github.com/vi/websocat); text frames only
websocat wss://api.feyn.fun/v1/ws
{"type":"hello","v":1,"codecs":[]}
{"type":"sub","id":1,"q":"B4HpTokrHXwYGQUYCN1axBH54pYf483Bpw5sCft2pump"}const ws = new WebSocket("wss://api.feyn.fun/v1/ws");
ws.binaryType = "arraybuffer";
ws.onopen = () => {
ws.send(JSON.stringify({ type: "hello", v: 1, codecs: ["deflate-raw"] }));
ws.send(JSON.stringify({ type: "sub", id: 1, q: "B4HpTokrHXwYGQUYCN1axBH54pYf483Bpw5sCft2pump" }));
};
ws.onmessage = async ({ data }) => {
if (typeof data === "string") return console.log(JSON.parse(data)); // control
const dv = new DataView(data);
const [codec, kind, sub] = [dv.getUint8(0), dv.getUint8(1), dv.getUint32(2, true)];
const body = new Uint8Array(data, 6);
const text = codec === 0
? new TextDecoder().decode(body)
: await new Response(new Blob([body]).stream().pipeThrough(new DecompressionStream("deflate-raw"))).text();
console.log(kind, sub, JSON.parse(text));
};import asyncio
import json
import struct
import zlib
import websockets
async def main():
async with websockets.connect("wss://api.feyn.fun/v1/ws") as ws:
await ws.send(json.dumps({"type": "hello", "v": 1, "codecs": ["deflate-raw"]}))
await ws.send(json.dumps({"type": "sub", "id": 1, "q": "B4HpTokrHXwYGQUYCN1axBH54pYf483Bpw5sCft2pump"}))
async for msg in ws:
if isinstance(msg, str):
print(json.loads(msg)) # control
continue
codec, kind, sub = struct.unpack_from("<BBI", msg)
body = msg[6:] if codec == 0 else zlib.decompress(msg[6:], -15)
print(kind, sub, json.loads(body))
asyncio.run(main())Messages
| Direction | type | What it does |
|---|---|---|
| client to server | hello | Must be first: {v: 1, codecs: ["deflate-raw"]}. You may send sub right after without waiting. |
| client to server | sub | Subscribe: id, q (a mint, curve, pool or link), optional rev to resume and want to pick sections. |
| client to server | unsub | Stop a sub by id. Frames already in flight are dropped by you. |
| client to server | get | A lazy chunk by ref; the answer is one binary chunk frame. |
| client to server | ping | Every 30 s while visible, with t; no reply within 15 s means the socket is dead. |
| server to client | hello_ack | Server time, max_subs, the codecs it will use, ping_interval_s. |
| server to client | resolved | First reply to every sub: what q is. Only mint, curve and pool go on to stream. |
| server to client | done | The first view (or resume) is complete at rev, with the hash of every section. |
| server to client | pong | Echoes t. |
| server to client | error | A code (below), with the sub's id when it concerns one. |
→ {"type":"hello","v":1,"codecs":["deflate-raw"]}
→ {"type":"sub","id":1,"q":"B4HpTokrHXwYGQUYCN1axBH54pYf483Bpw5sCft2pump"}
← {"type":"hello_ack","v":1,"server_time":1790416804123,"max_subs":4,"codecs":["deflate-raw"],"ping_interval_s":30}
← {"type":"resolved","id":1,"q":"B4HpTokrHXwYGQUYCN1axBH54pYf483Bpw5sCft2pump","kind":"mint","mint":"B4HpTokrHXwYGQUYCN1axBH54pYf483Bpw5sCft2pump","name":"Example Small Curve","symbol":"SMOL"}
← [01 01 01 00 00 00] summary summary · deflate-raw · 1022 B
← [00 02 01 00 00 00] section addrs · identity · 227 B
← [01 02 01 00 00 00] section malicious/M004 · deflate-raw · 541 B
← [00 02 01 00 00 00] section context · identity · 348 B
← [01 02 01 00 00 00] section tables/block0_buyers · deflate-raw · 422 B
← [01 02 01 00 00 00] section tables/holders · deflate-raw · 414 B
← {"type":"done","id":1,"rev":450641306003,"mode":"snapshot","hashes":{"addrs":"9bc01ae70874bc54","context":"ff7ccdae6732ed75","malicious/M004":"d9b2b17dca920c51","summary":"12ef3c252b3985d2","tables/block0_buyers":"15880e004c033b06","tables/holders":"0fab2cce775fb660"}}Binary frames
Every binary frame starts with a 6-byte header, then the payload: JSON, deflated with raw DEFLATE when it is 512 bytes or more and you listed the codec.
| Offset | Size | Field | Values |
|---|---|---|---|
| 0 | 1 | codec | 0 = identity (UTF-8 JSON), 1 = deflate-raw |
| 1 | 1 | kind | 1 = summary, 2 = section, 3 = patch, 4 = chunk |
| 2 | 4 | sub | u32 little-endian: the subscription id |
| 6 | n | payload | encoded per codec |
Process frames in arrival order (decompression is asynchronous). Drop frames with an unknown codec or kind, and frames for subs you no longer hold.
Limits and errors
Per socket: 4 subs, 10 sub/s, 20 get/s, 50 messages/s, frames up to 2,048 bytes. Per IP: 60 new sockets a minute, 30 lookups a minute (burst 15; a token you looked up in the last 10 minutes is free), 32 live subs. After 20 refused lookups in 10 minutes an IP is challenged (Turnstile). Reconnect with backoff 0.25 → 0.5 → 1 → 2 → 4 → 8 s, then resume each sub with its rev.
| error.code | Meaning | What to do |
|---|---|---|
bad_request | Malformed JSON, an unknown type, a missing field or a sub id that did not increase. The socket stays open. | Fix the client; don't retry blindly. |
bot_blocked | The client was detected as automated; close 1008 follows. The free WebSocket is for feyn.fun. | Use an API key or x402 (feyn.fun/pricing); don't reconnect until retry_after_ms. |
rate_limited | A sub or get was refused; retry_after_ms says when. msg "verification required" means the IP is challenged. | Retry after the delay; if challenged, solve Turnstile and reconnect with ?turnstile=. |
too_many_subs | The sub with this id was ended to make room (4 per socket). | Stop rendering it. |
unknown_ref | A get for a ref the server does not have. | Fall back to GET /v1/blob/{ref} once. |
overloaded | The server is shedding load; the sub did not start. | Back off and retry the sub. |
internal | An unexpected failure; the sub (if any) ended. | Retry the sub once after 1 s. |
hello_required | The first frame was not hello (sent just before close 4001). | Send hello first. |
unsupported_version | hello.v is not supported (sent just before close 4002). | Don't reconnect; update the client. |
| Close | Meaning | Reconnect |
|---|---|---|
| 1000 | Normal close (tab hidden over 5 min, navigation). | On demand |
| 1001 | Going away: restart or deploy. | Yes, back off from step 0 |
| 1003 | The client sent a binary frame. | No (client bug) |
| 1006 | Abnormal drop (network, Cloudflare restart). | Yes, back off |
| 1008 | Policy: inbound message flood, or bot_blocked (automated client). | Yes, start at 8 s; after bot_blocked, not before retry_after_ms |
| 1009 | Inbound frame over 2,048 bytes. | Yes, back off |
| 1011 | Internal error. | Yes, back off |
| 4001 | First frame was not hello. | Yes, after fixing the order |
| 4002 | hello.v unsupported. | No: reload the page |
| 4008 | Slow consumer. | Yes, back off |
| 4029 | Too many sockets from this IP. | Yes, start at 8 s |
Reference
Enterprise
Enterprise plan
Enterprise is $1,500 a month ($1,275 in $FEYN). It is bought like a pack, with POST /v1/keys?pack=enterprise or on the pricing page, and its key includes:
- The firehose (below): every finding as it happens, new, updated or retracted, across all pump.fun and PumpSwap tokens, on one WebSocket.
- Unlimited report lookups. Keyed calls spend no credits;
X-Feyn-Credits-Remainingreadsunlimited. - 2,000 requests/s, bursts of 4,000.
- 24/7 support in a private Telegram channel. Message @reaperofrugs on Telegram to set up your private support channel, before you buy or once you have the key.
/v1/pricingand/v1/keys/selfcarry the handle assupport_telegram.
Renewal. The key lasts 30 days from its payment. Paying again with the same key (renewal, or Renew on the key page) adds 30 days to it. The key, its firehose cursor and your setup stay the same, and nothing renews by itself. From 3 days before expiry, /v1/keys/self has renewal.due: true and the firehose sends a key_expiring notice.
WSwss://api.feyn.fun/v1/firehoseAvailable at launch
The Enterprise stream: one message per change to any token's findings or verdict, in order, each with a cursor to resume from. JSON text frames only, no subscriptions.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
Authorization | header | string | Bearer key of an Enterprise plan that has not expired. Or put the key in hello (clients that cannot set headers). |
hello.vrequired | body | 1 | Protocol version. |
hello.key | body | string | The key, when it is not in the header. |
hello.cursor | body | string | The last cursor you processed, to resume. Leave it out to start from now. |
Responses
- 101
- Upgraded. Send
hellowithin 10 s; the server answershello_ack, then streams. - 401
invalid_key,key_pendingorkey_expired(a key in the header is checked before upgrading).- 403
plan_required: the key is not Enterprise.- 429
too_many_sockets: 5 are open with this key.
A key sent in hello instead is checked there: an error, then a close code (below). The server re-reads the key every minute, so a renewal applies without reconnecting.
websocat -H "Authorization: Bearer $FEYN_API_KEY" wss://api.feyn.fun/v1/firehose
{"type":"hello","v":1,"cursor":"<last cursor>"}import WebSocket from "ws";
let cursor = loadCursor(); // undefined the first time
function connect() {
const ws = new WebSocket("wss://api.feyn.fun/v1/firehose", {
headers: { Authorization: `Bearer ${process.env.FEYN_API_KEY}` },
});
ws.on("open", () => ws.send(JSON.stringify({ type: "hello", v: 1, cursor })));
ws.on("message", (data) => {
const m = JSON.parse(data.toString());
if (m.type === "finding" || m.type === "verdict") queue.push(m); // handle elsewhere
if (m.type === "resync" || m.type === "gap") rereadTrackedReports();
if (m.cursor) saveCursor((cursor = m.cursor));
});
ws.on("close", () => setTimeout(connect, 1000 + Math.random() * 2000));
}
connect();import asyncio
import json
import os
import random
import websockets
async def main():
cursor = load_cursor() # None the first time
headers = {"Authorization": f"Bearer {os.environ['FEYN_API_KEY']}"}
while True:
try:
async with websockets.connect("wss://api.feyn.fun/v1/firehose", additional_headers=headers) as ws:
await ws.send(json.dumps({"type": "hello", "v": 1, "cursor": cursor}))
async for raw in ws:
m = json.loads(raw)
if m["type"] in ("finding", "verdict"):
queue.put_nowait(m) # handle elsewhere
elif m["type"] in ("resync", "gap"):
reread_tracked_reports()
if "cursor" in m:
cursor = m["cursor"]
save_cursor(cursor)
except websockets.ConnectionClosed:
await asyncio.sleep(1 + random.random() * 2)
asyncio.run(main()){
"type": "hello_ack",
"v": 1,
"start": "replay",
"cursor": "3f9a01c2.1834.1767225600123",
"server_time": 1767225600456,
"ping_interval_s": 30,
"max_lag": 32768,
"plan": "enterprise",
"expires_at": 1793008800,
"expires_in_s": 2591999,
"unlimited": true,
"firehose": true,
"renewal": {
"renewable": true,
"period_days": 30,
"due": false,
"notice_days": 3,
"how": "…"
},
"support_telegram": "@reaperofrugs"
}{
"type": "finding",
"event": "new",
"mint": "B4HpTokrHXwYGQUYCN1axBH54pYf483Bpw5sCft2pump",
"class": "S003",
"key": "scam/S003",
"scam": true,
"name": "Consolidated dump",
"headline": "…",
"severity": 5,
"confidence": "high",
"tier": null,
"first_detected_slot": 367000000,
"updated_slot": 367000123,
"token": {
"name": "…",
"symbol": "…"
},
"verdict": {
"level": "scam",
"scam": true,
"headline": "…"
},
"rev": 42,
"slot": 367000123,
"seq": 1835,
"cursor": "3f9a01c2.1835.1767225600789",
"t": 1767225600789
}Firehose messages
Every message is one JSON text frame with a type. Events carry seq, cursor (store the last one you processed) and t (unix ms). They describe state, not edits: treat each as an upsert keyed by (mint, key), so applying one twice is harmless. Ignore unknown fields and types.
| Direction | type | What it does |
|---|---|---|
| You → server | hello | First frame, within 10 s: protocol version, the key (unless it was in the Authorization header) and, to resume, the last cursor you processed. |
| Server → you | hello_ack | Where your stream starts (start), the head cursor, the ping interval, max_lag, and the key's plan, expiry, renewal and support contact. |
| Server → you | finding | A finding on a token changed: event is new, updated or retracted. Carries the finding's current fields and the token's verdict after the change. |
| Server → you | verdict | A token's verdict level changed with no finding change (from is the previous level). |
| Server → you | gap | Report builds were dropped before they could be diffed (lost_builds). Some changes may be missing: re-read the tokens you track. |
| Server → you | resync | Events were missed: reason cursor_expired or slow_consumer. Re-read the tokens you track. |
| Server → you | notice | key_expiring: the key expires within 3 days, with how to renew and the support contact. |
| Server → you | error | Before a close, or for a bad client message: code and msg. |
| You → server | ping | Answered with pong. The server pings every 30 s and closes a socket silent for 75 s. |
A finding names the token (mint, token), the finding (class, an S or M code, with name and headline), what happened to it (event) and the token's verdict after the change. For the evidence, fetch the report (GET /v1/token/{mint}, unlimited on this plan) or subscribe to the token on the WebSocket. A retracted finding no longer holds.
Resume and back-pressure
Resume. Reconnect, and send the last cursor you processed in hello.cursor. hello_ack.start says what happens:
live- No cursor: the stream starts from the next event.
replay- Your cursor is from this server process and still in its ring (65,536 events): exactly the events after it, in order.
approx- Your cursor is from the other app server or a previous process: replay by time from 2 s before it. Events may repeat; they are upserts.
resync- Your cursor is older than the ring or the process: a resync (cursor_expired) follows, then the live stream. Re-read the tokens you track.
Back-pressure. Every socket reads one shared, bounded ring at its own position, so a slow socket never delays the stream or anyone else. Fall more than max_lag (32,768) events behind, or leave a write blocked for 10 s, and you get resync (reason slow_consumer), then close 4008. Reconnect with your last processed cursor. Read in one task and process in another, so a slow handler does not stall the socket.
| Close | Meaning |
|---|---|
| 1000 | Normal, or idle. |
| 1003 | You sent a binary frame. |
| 1008 | Message flood (over 5/s sustained), or a malformed cursor. |
| 1009 | Frame over 4 KiB. |
| 1013 | The key store was unavailable at hello. Retry. |
| 4001 | No hello within 10 s, or the first frame was not hello. |
| 4002 | Unsupported protocol version. Use v: 1. |
| 4008 | Slow consumer. Reconnect with your last processed cursor. |
| 4029 | More than 5 sockets for this key. |
| 4401 | Unknown, malformed, revoked or pending key. |
| 4402 | Key expired: renew it, then reconnect with your cursor. |
| 4403 | The key's plan has no firehose (not Enterprise). |
Reconnect after any other close with a short random delay, and resume with your last cursor.
More
Changelog
- 28 Sep 2026
API v1, first public version. Reports (
feyn.report/1), the WebSocket (feyn.ws/1), API keys and x402 payments in USDC or in $FEYN at 15% off. - 29 Sep 2026
New plan lineup and Enterprise. Packs from $25; Enterprise at $1,500 a month with the firehose (
/v1/firehose), unlimited lookups and renewal of the same key. Additive:/v1/pricing,POST /v1/keysand/v1/keys/self(now also/v1/keys/me) gain fields. - 23 Oct 2026
Wallet clusters. Reports and summaries gain
token.clusters(the clusters object): the token's insiders and creator that belong to a group of wallets trading as one across many launches, with the group's confidence and figures. Additive. - 23 Oct 2026
Insider transfers followed. A wallet that received the token from an insider outside a trade now counts as an insider: its holdings and sells are the insiders', instead of the tokens counting as
sent.token.insidersgainsfollowed_wallets,followed_held_bpsandfollowed_sold_bps. Additive. - 5 Oct 2026
Outcomes and launch facts. Reports and summaries gain
token.outcomes(the outcomes object): historical outcome rates of comparable launches. The report'scontextgains the keyscreator_launches(the creator's other launches in the 24 hours before),creator_soldandholder_concentration. Informational: neither changes the verdict. Additive. - 2 Oct 2026
Insiders with unknown history.
token.insidersgainsunknown_walletsandfigures_unavailable: an insider whose trades on the token Feyn no longer holds is left out of the figures (markedat_least), never counted as 0. Additive. - 2 Oct 2026
Insiders, net of round trips.
token.insidersgainsnet_bought_bps,net_sold_bps,round_trip_bpsandheld_of_net_bought_bps(and per groupnet_bought_bps,net_sold_bps,received_bps,sent_bps, so each group adds up on its own): a wallet's buys it sold back no longer count as supply bought. The grossbought_bps/sold_bpskeep their meaning and cap at 100%; the site shows them while they stay under 100% and the net pair once they reach it. Additive. - 1 Oct 2026
Insiders. Reports and summaries gain
token.insiders(the insiders object): how much of the supply the wallets named by the findings bought, sold and still hold, and their realised profit. Additive. - Versioning
The path carries the major version (
/v1). New optional fields, message types, section keys and error codes are added without a new version, so ignore what you don't know. A breaking change moves to/v2.