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 $5 for 1,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),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. |
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.
Credits and pricing
Every call with a key spends credits. 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 |
| Pack | USDC | In $FEYN | Credits | Key lifetime |
|---|---|---|---|---|
| Starter | $5 | $4.25 | 1,000 | 30 days |
| Builder | $25 | $21.25 | 6,000 | 30 days |
| Pro | $100 | $85 | 30,000 | 60 days |
| Enterprise | $500 | $425 | 200,000 | 90 days |
| Single call (x402, no key) | $0.01 | $0.0085 | 1 report | — |
A key's lifetime counts from its payment. Credits left at expiry expire with it. Every pack gets the same data.
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 | 5 | 10 |
| Builder key | 10 | 20 |
| Pro key | 20 | 40 |
| Enterprise key | 200 | 400 |
| 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 | 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 | 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."
}
},
"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": "0a9aa2fe03249a92"
}
}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."
}
},
"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
}
]
}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, 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: 5000000 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": 1000,
"rate_limit": {
"per_s": 5,
"burst": 10
},
"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"]GET/v1/keys/selfAvailable at launch
Your key's credits, expiry, tier and paying transaction. Costs no credits (it does count against the tier's rate limit). 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.
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": 1000,
"credits_left": 830,
"expires_at": 1793008800,
"rate_limit": {
"per_s": 5,
"burst": 10
},
"transaction": "<signature>"
}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": "5",
"price_feyn_atoms": "42500000000",
"credits": 1000,
"lifetime_days": 30,
"rate_limit": {
"per_s": 5,
"burst": 10
}
},
{
"name": "builder",
"price_usdc": "25",
"price_feyn_atoms": "212500000000",
"credits": 6000,
"lifetime_days": 30,
"rate_limit": {
"per_s": 10,
"burst": 20
}
},
{
"name": "pro",
"price_usdc": "100",
"price_feyn_atoms": "850000000000",
"credits": 30000,
"lifetime_days": 60,
"rate_limit": {
"per_s": 20,
"burst": 40
}
},
{
"name": "enterprise",
"price_usdc": "500",
"price_feyn_atoms": "4250000000000",
"credits": 200000,
"lifetime_days": 90,
"rate_limit": {
"per_s": 200,
"burst": 400
}
}
],
"assets": {
"usdc": {
"mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"decimals": 6
},
"feyn": {
"mint": "<$FEYN mint>",
"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 · 763 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":"0a9aa2fe03249a92","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. |
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. | Yes, start at 8 s |
| 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 |
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. - 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.