Jevvis API · v1
Launch API
Launch a coin on pons from your own wallet, from code. The API builds the transaction; your wallet signs and sends it — Jevvis never holds a key or your funds. You hand back the transaction hash and it is verified on chain on the spot.
https://jevvis.app/api/v1Robinhood Chain · 4663JSON · UTF-8- Create a launch
POST /launches - Build its transaction
GET /launches/{id}/transaction - Sign & send it from your wallet
- Confirm with the hash
POST /launches/{id}/confirm
Authentication
Every request carries an API key in the Authorization header. A key belongs to one wallet and acts as it: launches you create with it are launched from that wallet, and only that wallet's launches are visible to it.
Create keys at API keys after signing in with the wallet. A key is shown once — we store only its hash — and can be revoked at any time. Up to 5 active keys per wallet.
Keep keys server-side. A key cannot move funds, but it can create launches for its wallet.
Authorization: Bearer jev_…Quickstart
The whole flow, end to end. Set JEVVIS_API_KEY to your key and LAUNCH_WALLET_KEY to the private key of the same wallet the API key belongs to — it stays in your program and signs locally.
import { createWalletClient, createPublicClient, defineChain, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
const API = "https://jevvis.app/api/v1";
const headers = {
Authorization: `Bearer ${process.env.JEVVIS_API_KEY}`,
"Content-Type": "application/json",
};
const chain = defineChain({
id: 4663, name: "Robinhood Chain",
nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
rpcUrls: { default: { http: ["https://rpc.mainnet.chain.robinhood.com"] } },
});
// Your program's own launching wallet — the one the API key belongs to.
const account = privateKeyToAccount(process.env.LAUNCH_WALLET_KEY);
const wallet = createWalletClient({ account, chain, transport: http() });
const client = createPublicClient({ chain, transport: http() });
async function api(path, init) {
const res = await fetch(API + path, { headers, ...init });
const body = await res.json();
if (!res.ok) throw new Error(`${body.error.code}: ${body.error.message}`);
return { status: res.status, body };
}
// 1. Create
const { body: launch } = await api("/launches", {
method: "POST",
body: JSON.stringify({ symbol: "CAT", name: "Cat Coin", pitch: "A coin for cats.", creatorTaxBps: 200 }),
});
// 2. Build the transaction (first buy: 0.01 ETH)
const { body: unsigned } = await api(`/launches/${launch.id}/transaction?devBuy=10000000000000000`);
const send = (tx) => wallet.sendTransaction({ to: tx.to, data: tx.data, value: BigInt(tx.value) });
if (unsigned.approve) {
await client.waitForTransactionReceipt({ hash: await send(unsigned.approve) });
}
// 3. Sign and send it yourself
const hash = await send(unsigned.tx);
// 4. Confirm — 202 while pending
for (;;) {
const { status, body } = await api(`/launches/${launch.id}/confirm`, {
method: "POST", body: JSON.stringify({ tx: hash }),
});
if (status !== 202) { console.log(body); break; }
await new Promise((r) => setTimeout(r, 3000));
}Fees
Every coin launched through Jevvis sets its pons creator wallet to a fee-split contract created for it, with holder fee sharing off. The split is fixed in the contract; nobody — including Jevvis — can change where the fees go.
| Who | Share of trade volume | |
|---|---|---|
| Launching wallet | 0.5% + launcher tax (your creatorTaxBps) | The rest of the pons creator share plus all of your tax, paid out through this coin's fee-split contract. |
| Jevvis ecosystem | 0.2% | Fixed. Taken from the pons creator share (0.7%), not added on top. |
| pons | 0.3% | Fixed, pons' own protocol share. |
A trader pays pons' 1% trading fee plus your launcher tax on every trade. Launching also costs the pons launch fee (paid in ETH, returned as launchFeeWei) plus your first buy.
Upload an image
POST/images
Stores a coin logo and returns its URL — the only kind of URL imageUrl accepts when you create a launch. Send multipart/form-data with one file field named image: PNG, JPEG or WebP, up to 5 MB. The type is read from the file's bytes, not its name or Content-Type. The same picture always gets the same URL.
Rate limit: 20 per 10 minutes per key
Body
imagefilerequired- The logo (multipart/form-data). PNG, JPEG or WebP, up to 5 MB.
Returns
- 201 Stored. Pass
urlasimageUrlwhen you create the launch.
Errors
- 400 No
imagefile, an empty file, or a file that is not PNG, JPEG or WebP. - 413 The file is over 5 MB.
- 503 Image upload is not available on this server.
curl -X POST https://jevvis.app/api/v1/images \
-H "Authorization: Bearer $JEVVIS_API_KEY" \
-F "image=@logo.png"{
"url": "https://jevvis.app/launch/img/9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08.png"
}Create a launch
POST/launches
Registers a coin to launch from the key's wallet. Nothing happens on chain yet: next, fetch the transaction and sign it. The launching wallet is always the key's wallet — there is no wallet field to set.
Rate limit: 10 per 10 minutes per key
Body
symbolstringrequired- Ticker: letters and digits, 1–10 characters (pons' rule). Upper-cased; a leading
$is dropped. namestringrequired- Coin name: letters, digits and single spaces, 1–32 characters (pons' rule).
pitchstringrequired- Description shown on pons, 1–256 characters. **No links** of any kind — pons rejects them.
creatorTaxBpsintegerrequired- Launcher tax in basis points, 0–1000 (0–10%). All of it is paid to the launching wallet. See Fees.
pairedAssetstringoptional- What the coin trades against, by symbol:
ETH,USDG, or any of pons' pair assets — stock tokens likeTSLA,NVDA,SPY, andcbBTC,TAO(70 in all). DefaultETH. xHandlestringoptional- X profile: an
x.com/<handle>(or twitter.com) profile URL, or the barehandle/@handle. Up to 15 letters, digits or_. Post URLs (/status/…) are rejected. telegramstringoptional- A
t.me/<name>URL of a public group or channel (5–32 letters, digits or_); bare usernames, invite links (t.me/+…,t.me/joinchat/…) and message links are rejected. Stored ashttps://t.me/<name>, the way pons puts it on chain. imageUrlstringoptional- Logo. Must be a
urlreturned by Upload an image (POST /images) — any other URL, including an image hosted elsewhere, is rejected with 400. The URL goes on chain for good. detailsobjectoptional- Optional notes, read by Jev as part of the launch package (what a name and pitch cannot say) — not on chain, shown on Jevvis as plain text. Keys (all optional):
origin,lore,timing,ecosystem,tech— originality, backstory, the current event, ecosystem (team, partners, backers), tech or product. How easy the joke is and who is already talking about it are read from the launcher's last 50 X posts instead; the old keysmemeandattentionare rejected with 400. Each a string, trimmed, at most 600 characters (UTF-16 code units, likepitch); an empty one is dropped. An unknown key, a non-string value or a control character (other than newline / tab) rejects the whole launch with 400. judgesstring[]optional- Paid naming: creator module ids from List creators, any number of them (all listed creators if you like). Each costs one
unitPriceMinor; the total is held from the key wallet's prepaid balance now, and whatever creators who cannot answer would have cost is refunded after judging. Requiresgroup. groupobjectoptional- Required with
judges:{ name, filters }— the push group, set up like a subscriber's group.name1–40 characters;filters{ match: "strict" | "standard" | "loose", minPass: 1–500, weights: { core, persona, tweet_style, coin_style } }— each named creator is compared with the coin question by question, the four cards weighted byweights(integers 0–100 adding up to exactly 100; anything else is a 400 that says what is wrong), and the coin is sent to every Jevvis user as a notification when at leastminPassof them pass thematchtier (see Thresholds). Omitted:loose, 1, 5 / 15 / 30 / 50. **Deprecated**, still accepted and converted whenmatchis absent (see the table under Thresholds):lean,tier,minBull,minAnswered. The group also appears on your subscriber page, switched off.
Returns
- 201 Created. Keep
id.
Errors
- 400 A field is missing or out of range, a
judgesid is not a listed creator, orjudgescame withoutgroup. The message names the field. - 402 The prepaid balance cannot cover
judges×unitPriceMinor. Nothing was created. - 503 The API is not configured.
curl -X POST https://jevvis.app/api/v1/launches \
-H "Authorization: Bearer $JEVVIS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"symbol": "CAT",
"name": "Cat Coin",
"pitch": "A coin for cats.",
"creatorTaxBps": 200,
"pairedAsset": "ETH",
"imageUrl": "https://jevvis.app/launch/img/9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08.png"
}'{
"id": "launch:6f0c2a1e-5b7d-4c7a-9a52-2d1f7c9e0b13",
"status": "draft",
"judges": 0,
"plan": "free",
"group": null,
"priceMinorMax": 0,
"ponsParams": {
"name": "Cat Coin",
"ticker": "CAT",
"description": "A coin for cats.",
"xProfile": null,
"telegram": null,
"pairedAsset": "ETH",
"fromWallet": "0x132c04f918ca698cff44b9a6d16a974453a2c01f",
"creatorTaxBps": 200,
"holderFeeSharing": false
}
}Get the launch transaction
GET/launches/{id}/transaction
Returns the unsigned transaction(s) the launching wallet signs and sends itself — we never hold a key. Send approve first when it is not null (any pair other than ETH, when the allowance is short) and wait for it to be mined, then send tx. The pons launch fee and pricing are read from chain when you call this, so build right before you sign.
Rate limit: 20 per 10 minutes per key
Path parameters
idstringrequired- The launch id returned by Create a launch (
launch:…). The bare part afterlaunch:works too.
Query parameters
devBuystring (integer)required- The first buy, in the paired asset's smallest unit: wei for ETH, 10⁻⁶ for USDG, 10⁻¹⁸ for stock tokens (the pair's own decimals). pons requires at least 1.
Returns
- 200
valueand amounts are decimal strings in wei / smallest units.
Errors
- 400
devBuyis missing, not an integer, or 0. - 404 No such launch for this key's wallet.
- 409 Already launched, or the launch's paired asset is no longer supported.
- 502 Reading pons from chain failed. Retry.
- 503 The launch contract is not live yet.
curl "https://jevvis.app/api/v1/launches/launch:6f0c2a1e-5b7d-4c7a-9a52-2d1f7c9e0b13/transaction?devBuy=10000000000000000" \
-H "Authorization: Bearer $JEVVIS_API_KEY"{
"id": "launch:6f0c2a1e-5b7d-4c7a-9a52-2d1f7c9e0b13",
"from": "0x132c04f918ca698cff44b9a6d16a974453a2c01f",
"pairedAsset": "ETH",
"approve": null,
"tx": {
"chainId": 4663,
"to": "0x4e4e…4e4e",
"data": "0xeafc4bc5…",
"value": "10500000000000000"
},
"launchFeeWei": "500000000000000",
"devBuy": "10000000000000000"
}Confirm a launch
POST/launches/{id}/confirm
Hand back the hash of the launch transaction you sent. It is verified on chain immediately — no waiting for an indexer. 202 means the transaction is not mined yet: call again in about 3 seconds. The hash only tells us where to look; every check reads chain data (it went through our launch contract, from this launch's wallet, with its tax).
Rate limit: 30 per minute per key
Path parameters
idstringrequired- The launch id returned by Create a launch (
launch:…). The bare part afterlaunch:works too.
Body
txstringrequired- The launch transaction hash,
0x+ 64 hex characters.
Returns
- 200 Launched and verified.
- 202 Not mined yet. Retry in ~3 s.
- 200 The transaction reverted. Nothing launched; build and send again.
- 200 Mined, but it is not this launch.
problemis display text naming the check that failed (see Launch object → mismatch).
Errors
- 400
txis not a transaction hash. - 404 No such launch for this key's wallet.
- 502 Reading the receipt failed. Retry.
- 503 The launch contract is not live yet.
curl -X POST https://jevvis.app/api/v1/launches/launch:6f0c2a1e-5b7d-4c7a-9a52-2d1f7c9e0b13/confirm \
-H "Authorization: Bearer $JEVVIS_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "tx": "0x5b1e…77c0" }'{ "state": "launched", "token": "0x9c3f…e41a", "tx": "0x5b1e…77c0" }{ "state": "pending" }{ "state": "failed" }{ "state": "mismatch", "problem": "…" }Retrieve a launch
GET/launches/{id}
One of the key's launches with its current status. Also finds launches confirmed later by our chain watcher, e.g. if your program stopped before confirming.
Rate limit: 60 per minute per key
Path parameters
idstringrequired- The launch id returned by Create a launch (
launch:…). The bare part afterlaunch:works too.
Returns
- 200 The launch object.
Errors
- 404 No such launch for this key's wallet.
curl https://jevvis.app/api/v1/launches/launch:6f0c2a1e-5b7d-4c7a-9a52-2d1f7c9e0b13 \
-H "Authorization: Bearer $JEVVIS_API_KEY"{
"id": "launch:6f0c2a1e-5b7d-4c7a-9a52-2d1f7c9e0b13",
"symbol": "CAT",
"name": "Cat Coin",
"pitch": "A coin for cats.",
"xHandle": "catcoin",
"xVerified": false,
"telegram": "https://t.me/catcoin",
"imageUrl": "https://jevvis.app/launch/img/9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08.png",
"details": { "origin": "The first cat coin on Robinhood Chain.", "lore": "A cat who lives on a rollup." },
"chain": "robinhood",
"launchpad": "pons_v2",
"creatorTaxBps": 200,
"status": "judged",
"token": "0x9c3f…e41a",
"launchTx": "0x5b1e…77c0",
"launchedMs": 1790000123000,
"plan": "paid",
"judges": 2,
"result": {
"asked": 3, "answered": 2, "noAnswer": 1,
"mean": 0.58, "min": 0.31, "max": 0.82, "spread": 0.51,
"match": {
"group": { "tier": "standard", "minPass": 1, "weights": { "core": 5, "persona": 15, "tweet_style": 30, "coin_style": 50 }, "passCount": 1, "compared": 2, "passed": true },
"members": [
{ "moduleId": "picks_3a9f0c2d1e7b4c55", "creator": "alice",
"match": { "cards": [
{ "card": "core", "matched": 2, "conflicted": 0, "unknown": 1, "bothAbsent": 0, "total": 3, "similarity": 1 },
{ "card": "persona", "matched": 4, "conflicted": 2, "unknown": 3, "bothAbsent": 3, "total": 12, "similarity": 0.6666666666666666 },
{ "card": "tweet_style", "matched": 3, "conflicted": 1, "unknown": 0, "bothAbsent": 0, "total": 4, "similarity": 0.75 },
{ "card": "coin_style", "matched": 9, "conflicted": 2, "unknown": 6, "bothAbsent": 8, "total": 25, "similarity": 0.8181818181818182 } ],
"total": 44, "match": 18, "conflict": 5, "unknown": 10, "redline": 0 },
"overall": { "coverage": 1, "similarity": 0.7840909090909091 },
"passes": { "strict": true, "standard": true, "loose": true } },
{ "moduleId": "0b6e4f2a-9d31-4c8e-a7f5-3e2d1c0b9a88", "creator": "bob",
"match": { "cards": [
{ "card": "core", "matched": 0, "conflicted": 1, "unknown": 1, "bothAbsent": 1, "total": 3, "similarity": 0 },
{ "card": "persona", "matched": 0, "conflicted": 0, "unknown": 9, "bothAbsent": 3, "total": 12, "similarity": null },
{ "card": "tweet_style", "matched": 2, "conflicted": 2, "unknown": 0, "bothAbsent": 0, "total": 4, "similarity": 0.5 },
{ "card": "coin_style", "matched": 5, "conflicted": 7, "unknown": 6, "bothAbsent": 7, "total": 25, "similarity": 0.4166666666666667 } ],
"total": 44, "match": 7, "conflict": 10, "unknown": 16, "redline": 0 },
"overall": { "coverage": 0.85, "similarity": 0.4215686274509804 },
"passes": { "strict": false, "standard": false, "loose": false } },
{ "moduleId": "picks_8e21d4b07c6f3a19", "creator": "carol",
"match": null, "overall": null,
"passes": { "strict": false, "standard": false, "loose": false } }
]
}
},
"wallet": "0x132c04f918ca698cff44b9a6d16a974453a2c01f",
"createdMs": 1790000000000,
"mismatch": null,
"judgeIds": ["picks_3a9f0c2d1e7b4c55", "0b6e4f2a-9d31-4c8e-a7f5-3e2d1c0b9a88"]
}List your launches
GET/launches
Every launch created from the key's wallet, newest first — including ones not launched yet and why a mismatch was rejected.
Rate limit: 60 per minute per key
Returns
- 200 OK.
curl https://jevvis.app/api/v1/launches \
-H "Authorization: Bearer $JEVVIS_API_KEY"{
"launches": [ { "id": "launch:6f0c2a1e-5b7d-4c7a-9a52-2d1f7c9e0b13", "status": "launched", … } ]
}List creators
GET/creators
The creator modules a launch may name in judges, and the price per named creator (unitPriceMinor, in 10⁻⁶ USD). freeMax is 0: every named creator is paid. creator is the creator's current X handle for display — null while it is not known yet; name creators by module id, never by handle. compare says whether the creator can be compared at all (see Thresholds): spec is the question set it was counted against, cards how many questions each card has a number for — core, persona, tweet_style, coin_style — null for a card with too few posts (every coin reads can't tell there). compare is null for a module not yet recomputed into a comparison profile. The creator's own numbers and the per-question verdicts are on each coin's page, for those who may see that creator. origin is official for a module Jevvis made from its own research, self for one the creator made; sourceRunId is the X analysis it was built from (null if none); cardRules is the version of the card-to-module rules it was built under (null when it was not built from a card; the current version is 5; a lower one is being rebuilt from its source analysis under the current rules — there is one set of rules, not two).
Rate limit: 60 per minute per key
Returns
- 200 OK.
curl https://jevvis.app/api/v1/creators \
-H "Authorization: Bearer $JEVVIS_API_KEY"{
"freeMax": 0,
"unitPriceMinor": 1000,
"judges": [
{ "id": "picks_3a9f0c2d1e7b4c55", "name": null, "creator": "alice", "origin": "official", "sourceRunId": "7c1d9e3b-2a4f-4b8c-9e6d-5f0a1b2c3d4e", "cardRules": 5,
"compare": { "spec": "64ed5774519164434e57725b7033f1048e69ae9d226ee143ae2cf603a5f45ccb",
"cards": { "core": 6, "persona": 12, "tweet_style": 7, "coin_style": 25 } } },
{ "id": "0b6e4f2a-9d31-4c8e-a7f5-3e2d1c0b9a88", "name": "Degen picks", "creator": null, "origin": "self", "sourceRunId": "0b6e4f2a-9d31-4c8e-a7f5-3e2d1c0b9a88", "cardRules": 5, "compare": null }, …
]
}The launch object
Returned by Retrieve and List. Amounts are never floats; times are Unix milliseconds.
Attributes
idstringrequiredlaunch:+ uuid.statusstringrequireddraft(created, not launched) ·launched(verified on chain) ·judging(creator modules are reading it now) ·judged(at least one creator module has read it; more may still read it later) ·withdrawn(taken down).symbol, name, pitchstringrequired- As created.
creatorTaxBpsintegerrequired- As created, 0–1000.
tokenstring | nullrequired- Token address once launched.
launchTxstring | nullrequired- Launch transaction hash once launched.
launchedMs, createdMsinteger | nullrequired- Unix milliseconds.
walletstringrequired- The launching wallet (the key's wallet).
mismatchobject | nullrequired- Owner only. The last transaction that was not accepted as this launch:
problem(display text, one per check: wrong name or ticker, launched before this launch was created, already claimed by another launch, not sent through the Jevvis launch contract, tax differs, no coin created, sent from another wallet) plus what was found on chain (token,txHash,name,symbol,launchedSec). Branch onstatus, not onproblem. xHandle, telegram, imageUrlstring | nullrequired- As created.
detailsobject | nullrequired- As created: only the fields that were filled, trimmed;
nullwhen none. Plain text. chain, launchpadstringrequiredrobinhood,pons_v2.xVerifiedbooleanrequired- Whether you verified the X account. Only a verified account counts as evidence when creators judge the coin.
planstringrequiredfree(no creators named: launching costs nothing beyond pons' launch fee and gas) orpaid(creators named injudges).judges, judgeIdsinteger, string[]required- How many creators the launch named, and which (
judgeIds, owner only). Besides them, every creator module someone subscribes to reads each verified launch automatically;resultcounts all of them. resultobject | nullrequirednulluntil judged. Then{ asked, answered, noAnswer, match, mean, min, max, spread }.askedcreators read it,answeredof them could. The four cards question by question, with each side's evidence, are on the coin's page,/coin/{token}.result.match.groupobject | nullrequired- The launch group's threshold result, the same shape as a webhook's
data.match:tier,minPass,weights,passCount,compared,passed.nullwhen the launch named no creators (no group). result.match.membersarrayrequired- One per creator who read it.
moduleIdandcreator(display handle ornull) are included only for the launch's own submitter; everyone else gets the same entries without them, sorted by the numbers rather than by creator.match.cards— per cardmatched,conflicted,unknown,bothAbsent,total,similarity(matchnull= no comparison profile yet: never counted as a pass);overall{ coverage, similarity }with the launch group's weights (the default without a group);passes, whether they pass each tier (strict,standard,loose).total,match,conflict,unknown,redlineare **deprecated** F016 fields kept for older receivers: the four cards added up,redlinealways 0. result.mean, min, max, spreadnumber | nullrequired- Stances 0–1 (≥ 0.6 bullish, < 0.4 bearish);
meanis the weighted average of only the creators who could answer — one vote each at least, more for more subscribers — and isnullif none could;spread= max − min. **Deprecated** (derived from the old bullish scorepBuy): still sent, removed once every creator module has been recomputed. Usematch.
Thresholds
Creators do not score a coin. The same Jev questions are asked of the creator and of the coin, each pair is compared question by question in four cards, and a group decides from the weighted overall. The long version, with examples and the evidence rules, is How matching works.
- The same Jev questions are asked of both sides. A creator's side is counted from their own posts into four cards (
corebase profile,personapersonality,tweet_styleposting,coin_stylecoin picking): a share of posts for a yes/no question, a distribution for a one-choice question, low / mid / high bands for a 0–3 score. A new coin's side is one read of its whole launch package — name, ticker, description, the optionaldetails, and the launcher's X bio and posts — with the same questions and options, the subject being “this launch”. No post is cut short; only when the package is too long are the posts with the fewest likes + reposts + replies + quotes dropped, and the coin page says how many. - Each question is one of: **alike**, **unlike** — the coin lacks what the creator often has (
missing) or has what they almost never touch (avoids) — **neither has it** (both_absent: listed, not scored) or **can't tell** (unknown). Each point carriesscored: five questions (main field, topic field, sentiment, conviction, falsifiable) arefalse— judged and listed, but left out of every count and of similarity, because creators in one field answer them alike. A yes/no question: the creator often has it at ≥ 10% of posts, almost never under 5%; in between is can't tell. A one-choice or score question: the coin's answer at ≥ 20% of the creator's distribution is alike, under 5% unlike, in between can't tell; the coin answering “none”, or Jev being unsure, is can't tell. - Per card:
matched(alike),conflicted(unlike),unknown,bothAbsent,total(all four added — scored questions only), andsimilarity= matched ÷ (matched + conflicted),nullwhen both are 0. - Overall: the cards' similarities weighted by the four card weights — integers 0–100 adding up to 100, default 5 / 15 / 30 / 50 (core / persona / tweet_style / coin_style), set per group in
filters.weights. A card that can't be told, or weighs 0, drops out and the rest are re-weighted.coverage= the weights of the cards that count ÷ 100; under 50%overall.similarityisnull. - Not compared (long-run behaviour, or the same for every coin): subject over time, following up on calls, posting rhythm, titles, token-talk share, coins called, repeat calls, chain, launchpad. The launcher's followers and likes / reposts / replies / quotes (mean and median) are shown on the coin page, never compared.
- ⚠️ Every threshold above (10% / 5%, 20% / 5%, coverage 50%, the three tiers) is provisional until calibrated against labelled data; the numbers will change, the fields will not.
- A group passes when at least
minPassof its creators pass the chosen tier with the group's weights. A creator without a comparison profile (module not recomputed yet) does not pass. - Below the threshold: the coin is still charged (the judging was done) but it is not pushed and no webhook is sent; it shows under “Below threshold” on the notifications page with what fell short.
| match | Name | A creator passes when |
|---|---|---|
strict | Strict | Overall similarity ≥ 75% (overall.similarity ≥ 0.75). Coverage under 50% never passes. |
standard | Standard | Overall similarity ≥ 60% (overall.similarity ≥ 0.6). Coverage under 50% never passes. |
loose | Loose | Overall similarity ≥ 50% (overall.similarity ≥ 0.5). Coverage under 50% never passes. |
Old filters (deprecated): a group saved with lean / minBull is read through this table until its owner confirms; see Confirm a group's threshold.
| Old | Now |
|---|---|
lean: "all" | match: "loose" |
lean: "not_bear" | match: "standard" |
lean: "bull" | match: "strict" |
minBull: n | minPass: max(1, n) |
tier, minAnswered | No counterpart: whether a creator's questions can be compared is already part of every tier (coverage). |
no weights | weights { "core": 5, "persona": 15, "tweet_style": 30, "coin_style": 50 } (the default) |
Confirm a group's threshold
POST/groups/{id}/confirm-match
A group saved before the three-tier threshold existed was converted from its old lean / minBull (see Thresholds) and is marked matchNotice: true until its owner confirms. This confirms it: the converted match and minPass are written into its filters and matchNotice turns false. Same as the Confirm button on the subscriber page. The group id is data.groupId in a webhook event. Only the key wallet's own groups.
Rate limit: 10 per 10 minutes per key
Path parameters
idstringrequired- The group id.
Returns
- 200 The group as saved.
Errors
- 404 No such group for this key's wallet.
curl -X POST https://jevvis.app/api/v1/groups/3f1c…/confirm-match \
-H "Authorization: Bearer $JEVVIS_API_KEY"{
"group": {
"id": "3f1c…", "name": "My bulls", "enabled": true, "launchId": null, "matchNotice": false,
"filters": { "match": "strict", "minPass": 2, "weights": { "core": 5, "persona": 15, "tweet_style": 30, "coin_style": 50 }, "lean": "bull", "tier": "all", "minBull": 2, "minAnswered": 0 },
"members": [ … ], "spend": { … }
}
}Webhooks
Instead of polling, register a URL and we push each new notification to it the moment it exists: one per coin and group when creators in one of your groups judge a new launch (with what that group was charged and whether it passed the group's filters), and a broadcast when a launcher's paid push goes out to every user. If a coin is charged again later, you getnotification.updated for the same notification. Each delivery is a POST with a JSON body, signed with the secret you got when registering.
Event attributes
idstringrequiredntf_+ the notification id; an update isntf_<id>_r<n>. The same event can arrive more than once (a retry after your endpoint timed out); use this to ignore repeats.typestringrequirednotification.createdthe first time.notification.updatedwhen a notification you already got changed — a coin is often charged in two batches, so the amount (and the group's read) can grow; the new version carries the samedata.idand replaces the old one.data.idintegerrequired- The notification. One per coin × group: an update reuses it.
data.kindstringrequiredgroup: creators in one of your own groups judged the coin (one notification per coin × group).launch: a broadcast — a launcher paid a group of creators to judge it, and the result is pushed to every user.charge: only in notifications from before 2026-09-27 (one per charge).data.proactivebooleanrequiredtruefor a broadcast (kind: launch): you did not subscribe to anything to get it.data.groupNameis the launcher's group that judged it.data.chargedMinorinteger | nullrequiredgroup: what this group's card spent on this coin, in millionths of a dollar (the group card's own figure; a creator you have in two groups is counted on both).nullfor a broadcast.data.passedboolean | nullrequiredgroup: whether the coin passed that group's threshold (data.match). A coin that did not pass is still charged but is not sent here — it only shows under “Below threshold” on the notifications page; you getnotification.updatedwithfalseonly if one you already received stopped passing.nullfor a broadcast.data.matchobject | nullrequired- The group's threshold result:
tier(strict/standard/loose),minPass,weights(the group's four card weights),passCount(members who passed),compared(members with a comparison) andpassed. Each member's result isdata.coin.members[]:match.cards— per cardmatched,conflicted,unknown,bothAbsent,total,similarity(see Thresholds) —overall{ coverage, similarity }with the group's weights (similaritynullwhen coverage is short), andpasses;matchnull= no comparison profile yet.total,match,conflict,unknown,redlineare **deprecated** F016 fields kept for older receivers: the four cards added up,redlinealways 0. A notification from before the four cards carries only those deprecated fields.nullfor a broadcast from before the threshold existed. data.coin.judged, data.coin.countsinteger, objectrequired- How many of the group's creators judged it (
judged), and how many leaned bullish / neutral / bearish (counts— **Deprecated** (derived from the old bullish scorepBuy): still sent, removed once every creator module has been recomputed. Usematch.). A snapshot at the moment it was judged. data.coin.score, data.coin.supportnumber | nullrequiredscore: the weighted mean stance (pBuy) of the creators who could answer — one vote each at least, more for more subscribers.support: the mean evidence support of those readings.nullwhen nobody could answer. **Deprecated** (derived from the old bullish scorepBuy): still sent, removed once every creator module has been recomputed. Usematch.data.coin.membersarrayrequired- The creators behind the counts:
moduleId,creator(display handle ornull),match,overallandpasses(seedata.match), and the deprecatedpBuy(null= no reading;0= a veto) andlean. A broadcast lists only the creators you can see (you subscribe to them); the counts still include everyone. data.imageUrlstring | nullrequired- The coin's launch image, or
null. data.urlstringrequired- The coin's analysis page.
| Header | Meaning |
|---|---|
Jevvis-Signature | t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>" with your secret>. |
Jevvis-Event-Id | Same as the body's id. |
Jevvis-Delivery | This attempt's delivery id, for support. |
- Answer any 2xx within 10 seconds. Anything else — another status, a timeout, a redirect — is a failure. Redirects are not followed.
- A failed delivery is retried after 30 s, 2 min, 10 min, 1 h and 6 h, then given up.
- After 20 failures in a row the webhook is switched off; re-enable it once your endpoint works.
- Verify the signature on the raw body before parsing it, and reject a
tmore than 5 minutes old.
{
"id": "ntf_812",
"type": "notification.created",
"createdMs": 1790000200000,
"data": {
"id": 812,
"kind": "group",
"proactive": false,
"chargedMinor": 3000,
"passed": true,
"groupId": "3f1c…",
"groupName": "My bulls",
"coin": {
"token": "0x9c3f…e41a", "symbol": "CAT", "name": "Cat Coin", "launchedMs": 1790000123000,
"judged": 5, "counts": { "bull": 3, "neutral": 1, "bear": 1 }, "score": 0.64, "support": 0.8,
"members": [ { "moduleId": "picks_3a9f0c2d1e7b4c55", "creator": "alice", "pBuy": 0.72, "lean": "bull",
"match": { "cards": [
{ "card": "core", "matched": 2, "conflicted": 0, "unknown": 1, "bothAbsent": 0, "total": 3, "similarity": 1 },
{ "card": "persona", "matched": 4, "conflicted": 2, "unknown": 3, "bothAbsent": 3, "total": 12, "similarity": 0.6666666666666666 },
{ "card": "tweet_style", "matched": 3, "conflicted": 1, "unknown": 0, "bothAbsent": 0, "total": 4, "similarity": 0.75 },
{ "card": "coin_style", "matched": 9, "conflicted": 2, "unknown": 6, "bothAbsent": 8, "total": 25, "similarity": 0.8181818181818182 } ],
"total": 44, "match": 18, "conflict": 5, "unknown": 10, "redline": 0 },
"overall": { "coverage": 1, "similarity": 0.7840909090909091 }, "passes": true } ],
"match": { "tier": "standard", "minPass": 1, "weights": { "core": 5, "persona": 15, "tweet_style": 30, "coin_style": 50 }, "passCount": 1, "compared": 1, "passed": true }
},
"match": { "tier": "standard", "minPass": 1, "weights": { "core": 5, "persona": 15, "tweet_style": 30, "coin_style": 50 }, "passCount": 1, "compared": 1, "passed": true },
"imageUrl": "https://jevvis.app/launch/img/9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08.png",
"url": "https://jevvis.app/coin/0x9c3f…e41a"
}
}import { createHmac, timingSafeEqual } from "node:crypto";
// rawBody: the request body exactly as received (a string), before JSON.parse.
export function verifyJevvis(rawBody, header, secret, toleranceSec = 300) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const t = Number(parts.t);
if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const got = Buffer.from(parts.v1 ?? "", "hex");
const want = Buffer.from(expected, "hex");
return got.length === want.length && timingSafeEqual(got, want);
}Register a webhook
POST/webhooks
Every new notification of the key's wallet — its own groups' reads and launch pushes to every user — is POSTed to this URL as it happens, signed. Only notifications after registering are sent. Keep the returned secret: it is shown once, and it is how you verify each delivery (see Webhooks).
Rate limit: 10 per 10 minutes per key
Body
urlstringrequired- An https URL on port 443 of a public host, up to 500 characters. Up to 3 per wallet. Private, loopback and link-local addresses are refused — also when the host later resolves to one.
Returns
- 201 Store
secretnow; it is never shown again.
Errors
- 400
urlis not an https URL on 443 of a public host. - 409 The wallet already has 3 webhooks. Delete one first.
curl -X POST https://jevvis.app/api/v1/webhooks \
-H "Authorization: Bearer $JEVVIS_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "url": "https://hooks.example.com/jevvis" }'{
"id": "wh_2b7e9c1a-4f0d-4e8b-9c55-0a6d3e1f8b24",
"url": "https://hooks.example.com/jevvis",
"createdMs": 1790000000000,
"disabledMs": null,
"disabledWhy": null,
"failures": 0,
"recent": [],
"secret": "whsec_…"
}List webhooks
GET/webhooks
The key wallet's webhooks with their last 20 deliveries. A webhook switched off after repeated failures has disabledMs and the reason.
Rate limit: 60 per minute per key
Returns
- 200
secretis never listed.
curl https://jevvis.app/api/v1/webhooks \
-H "Authorization: Bearer $JEVVIS_API_KEY"{
"webhooks": [{
"id": "wh_2b7e9c1a-4f0d-4e8b-9c55-0a6d3e1f8b24", "url": "https://hooks.example.com/jevvis", "createdMs": 1790000000000,
"disabledMs": null, "disabledWhy": null, "failures": 0,
"recent": [ { "notificationId": 812, "state": "delivered", "attempts": 1, "lastStatus": 204, "lastError": null } ]
}]
}Delete a webhook
DELETE/webhooks/{id}
Stops pushing to that URL. Deliveries still waiting to be retried are dropped.
Rate limit: 10 per 10 minutes per key
Path parameters
idstringrequired- The webhook id (
wh_…).
Returns
- 200 OK.
Errors
- 404 No such webhook for this key's wallet.
curl -X DELETE https://jevvis.app/api/v1/webhooks/wh_2b7e9c1a-4f0d-4e8b-9c55-0a6d3e1f8b24 \
-H "Authorization: Bearer $JEVVIS_API_KEY"{ "deleted": "wh_2b7e9c1a-4f0d-4e8b-9c55-0a6d3e1f8b24" }Re-enable a webhook
POST/webhooks/{id}/enable
A webhook is switched off after 20 failed deliveries in a row. Fix your endpoint, then call this: pushing resumes from new notifications (the ones missed while it was off are not replayed; they are still in the inbox).
Rate limit: 10 per 10 minutes per key
Path parameters
idstringrequired- The webhook id (
wh_…).
Returns
- 200 OK.
Errors
- 404 No such webhook for this key's wallet.
curl -X POST https://jevvis.app/api/v1/webhooks/wh_2b7e9c1a-4f0d-4e8b-9c55-0a6d3e1f8b24/enable \
-H "Authorization: Bearer $JEVVIS_API_KEY"{ "enabled": "wh_2b7e9c1a-4f0d-4e8b-9c55-0a6d3e1f8b24" }Errors
Errors use conventional HTTP status codes and one body shape. Branch on error.code; error.message is for people and may change (some messages are in Chinese).
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_request | The request is malformed or a field is invalid. message is display text and may be in Chinese; branch on code and the HTTP status. |
| 401 | unauthorized | No API key, a malformed one, or a revoked one. |
| 402 | insufficient_balance | The prepaid balance cannot cover the creators named in judges. Top up on the subscriber page. |
| 404 | not_found | No such launch for this key's wallet. Other wallets' launches are never visible. |
| 409 | conflict | The launch is not in a state that allows this (e.g. already launched). |
| 413 | payload_too_large | The uploaded image is over 5 MB. |
| 429 | rate_limited | Rate limited. Wait Retry-After seconds; requests are not queued. |
| 502 | upstream_error | Chain or an internal service did not answer. Safe to retry. |
| 503 | unavailable | The launch contract is not live yet, or the API is not configured. |
{
"error": {
"code": "not_found",
"message": "no such launch"
}
}Rate limits
Limits are sliding windows. Every response carries the current state; over the limit you get 429 withRetry-After in seconds. Requests are not queued — wait and retry.
| Scope | Limit |
|---|---|
| Per IP address, before the key is checked | 120 per minute |
| Upload an image (per key) | 20 per 10 minutes |
| Create a launch (per key) | 10 per 10 minutes |
| Get the launch transaction (per key) | 20 per 10 minutes |
| Confirm a launch (per key) | 30 per minute |
| Retrieve / list (per key) | 60 per minute |
Transaction and confirm calls read a shared chain node, so they also share a service-wide cap; you may see a 429 before your own limit when it is busy.
X-RateLimit-Limit: 30
X-RateLimit-Remaining: 27
X-RateLimit-Reset: 41
Retry-After: 41 # only on 429