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.

Base URLhttps://jevvis.app/api/v1
ChainRobinhood Chain · 4663
FormatJSON · UTF-8
  1. Create a launch POST /launches
  2. Build its transaction GET /launches/{id}/transaction
  3. Sign & send it from your wallet
  4. 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.

Header
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.

WhoShare of trade volume
Launching wallet0.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 ecosystem0.2%Fixed. Taken from the pons creator share (0.7%), not added on top.
pons0.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 url as imageUrl when you create the launch.

Errors

  • 400 No image file, 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.
POST /images
curl -X POST https://jevvis.app/api/v1/images \
  -H "Authorization: Bearer $JEVVIS_API_KEY" \
  -F "image=@logo.png"
Response · 201
{
  "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 like TSLA, NVDA, SPY, and cbBTC, TAO (70 in all). Default ETH.
xHandlestringoptional
X profile: an x.com/<handle> (or twitter.com) profile URL, or the bare handle / @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 as https://t.me/<name>, the way pons puts it on chain.
imageUrlstringoptional
Logo. Must be a url returned 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 keys meme and attention are rejected with 400. Each a string, trimmed, at most 600 characters (UTF-16 code units, like pitch); 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. Requires group.
groupobjectoptional
Required with judges: { name, filters } — the push group, set up like a subscriber's group. name 1–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 by weights (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 least minPass of them pass the match tier (see Thresholds). Omitted: loose, 1, 5 / 15 / 30 / 50. **Deprecated**, still accepted and converted when match is 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 judges id is not a listed creator, or judges came without group. The message names the field.
  • 402 The prepaid balance cannot cover judges × unitPriceMinor. Nothing was created.
  • 503 The API is not configured.
POST /launches
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"
  }'
Response · 201
{
  "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 after launch: 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 value and amounts are decimal strings in wei / smallest units.

Errors

  • 400 devBuy is 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.
GET /launches/{id}/transaction
curl "https://jevvis.app/api/v1/launches/launch:6f0c2a1e-5b7d-4c7a-9a52-2d1f7c9e0b13/transaction?devBuy=10000000000000000" \
  -H "Authorization: Bearer $JEVVIS_API_KEY"
Response · 200
{
  "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 after launch: 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. problem is display text naming the check that failed (see Launch object → mismatch).

Errors

  • 400 tx is 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.
POST /launches/{id}/confirm
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" }'
Response · 200
{ "state": "launched", "token": "0x9c3f…e41a", "tx": "0x5b1e…77c0" }
Response · 202
{ "state": "pending" }
Response · 200
{ "state": "failed" }
Response · 200
{ "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 after launch: works too.

Returns

  • 200 The launch object.

Errors

  • 404 No such launch for this key's wallet.
GET /launches/{id}
curl https://jevvis.app/api/v1/launches/launch:6f0c2a1e-5b7d-4c7a-9a52-2d1f7c9e0b13 \
  -H "Authorization: Bearer $JEVVIS_API_KEY"
Response · 200
{
  "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.
GET /launches
curl https://jevvis.app/api/v1/launches \
  -H "Authorization: Bearer $JEVVIS_API_KEY"
Response · 200
{
  "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.
GET /creators
curl https://jevvis.app/api/v1/creators \
  -H "Authorization: Bearer $JEVVIS_API_KEY"
Response · 200
{
  "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

idstringrequired
launch: + uuid.
statusstringrequired
draft (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 on status, not on problem.
xHandle, telegram, imageUrlstring | nullrequired
As created.
detailsobject | nullrequired
As created: only the fields that were filled, trimmed; null when none. Plain text.
chain, launchpadstringrequired
robinhood, pons_v2.
xVerifiedbooleanrequired
Whether you verified the X account. Only a verified account counts as evidence when creators judge the coin.
planstringrequired
free (no creators named: launching costs nothing beyond pons' launch fee and gas) or paid (creators named in judges).
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; result counts all of them.
resultobject | nullrequired
null until judged. Then { asked, answered, noAnswer, match, mean, min, max, spread }. asked creators read it, answered of 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. null when the launch named no creators (no group).
result.match.membersarrayrequired
One per creator who read it. moduleId and creator (display handle or null) 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 card matched, conflicted, unknown, bothAbsent, total, similarity (match null = 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, redline are **deprecated** F016 fields kept for older receivers: the four cards added up, redline always 0.
result.mean, min, max, spreadnumber | nullrequired
Stances 0–1 (≥ 0.6 bullish, < 0.4 bearish); mean is the weighted average of only the creators who could answer — one vote each at least, more for more subscribers — and is null if none could; spread = max − min. **Deprecated** (derived from the old bullish score pBuy): still sent, removed once every creator module has been recomputed. Use match.

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 (core base profile, persona personality, tweet_style posting, coin_style coin 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 optional details, 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 carries scored: five questions (main field, topic field, sentiment, conviction, falsifiable) are false — 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), and similarity = matched ÷ (matched + conflicted), null when 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.similarity is null.
  • 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 minPass of 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.
matchNameA creator passes when
strictStrictOverall similarity ≥ 75% (overall.similarity ≥ 0.75). Coverage under 50% never passes.
standardStandardOverall similarity ≥ 60% (overall.similarity ≥ 0.6). Coverage under 50% never passes.
looseLooseOverall 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.

OldNow
lean: "all"match: "loose"
lean: "not_bear"match: "standard"
lean: "bull"match: "strict"
minBull: nminPass: max(1, n)
tier, minAnsweredNo counterpart: whether a creator's questions can be compared is already part of every tier (coverage).
no weightsweights { "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.
POST /groups/{id}/confirm-match
curl -X POST https://jevvis.app/api/v1/groups/3f1c…/confirm-match \
  -H "Authorization: Bearer $JEVVIS_API_KEY"
Response · 200
{
  "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

idstringrequired
ntf_ + the notification id; an update is ntf_<id>_r<n>. The same event can arrive more than once (a retry after your endpoint timed out); use this to ignore repeats.
typestringrequired
notification.created the first time. notification.updated when 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 same data.id and replaces the old one.
data.idintegerrequired
The notification. One per coin × group: an update reuses it.
data.kindstringrequired
group: 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.proactivebooleanrequired
true for a broadcast (kind: launch): you did not subscribe to anything to get it. data.groupName is the launcher's group that judged it.
data.chargedMinorinteger | nullrequired
group: 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). null for a broadcast.
data.passedboolean | nullrequired
group: 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 get notification.updated with false only if one you already received stopped passing. null for 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) and passed. Each member's result is data.coin.members[]: match.cards — per card matched, conflicted, unknown, bothAbsent, total, similarity (see Thresholds) — overall { coverage, similarity } with the group's weights (similarity null when coverage is short), and passes; match null = no comparison profile yet. total, match, conflict, unknown, redline are **deprecated** F016 fields kept for older receivers: the four cards added up, redline always 0. A notification from before the four cards carries only those deprecated fields. null for 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 score pBuy): still sent, removed once every creator module has been recomputed. Use match.). A snapshot at the moment it was judged.
data.coin.score, data.coin.supportnumber | nullrequired
score: 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. null when nobody could answer. **Deprecated** (derived from the old bullish score pBuy): still sent, removed once every creator module has been recomputed. Use match.
data.coin.membersarrayrequired
The creators behind the counts: moduleId, creator (display handle or null), match, overall and passes (see data.match), and the deprecated pBuy (null = no reading; 0 = a veto) and lean. 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.
HeaderMeaning
Jevvis-Signaturet=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>" with your secret>.
Jevvis-Event-IdSame as the body's id.
Jevvis-DeliveryThis 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 t more than 5 minutes old.
Event
{
  "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 secret now; it is never shown again.

Errors

  • 400 url is not an https URL on 443 of a public host.
  • 409 The wallet already has 3 webhooks. Delete one first.
POST /webhooks
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" }'
Response · 201
{
  "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 secret is never listed.
GET /webhooks
curl https://jevvis.app/api/v1/webhooks \
  -H "Authorization: Bearer $JEVVIS_API_KEY"
Response · 200
{
  "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.
DELETE /webhooks/{id}
curl -X DELETE https://jevvis.app/api/v1/webhooks/wh_2b7e9c1a-4f0d-4e8b-9c55-0a6d3e1f8b24 \
  -H "Authorization: Bearer $JEVVIS_API_KEY"
Response · 200
{ "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.
POST /webhooks/{id}/enable
curl -X POST https://jevvis.app/api/v1/webhooks/wh_2b7e9c1a-4f0d-4e8b-9c55-0a6d3e1f8b24/enable \
  -H "Authorization: Bearer $JEVVIS_API_KEY"
Response · 200
{ "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).

StatusCodeMeaning
400invalid_requestThe request is malformed or a field is invalid. message is display text and may be in Chinese; branch on code and the HTTP status.
401unauthorizedNo API key, a malformed one, or a revoked one.
402insufficient_balanceThe prepaid balance cannot cover the creators named in judges. Top up on the subscriber page.
404not_foundNo such launch for this key's wallet. Other wallets' launches are never visible.
409conflictThe launch is not in a state that allows this (e.g. already launched).
413payload_too_largeThe uploaded image is over 5 MB.
429rate_limitedRate limited. Wait Retry-After seconds; requests are not queued.
502upstream_errorChain or an internal service did not answer. Safe to retry.
503unavailableThe launch contract is not live yet, or the API is not configured.
Error · 404
{
  "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.

ScopeLimit
Per IP address, before the key is checked120 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.

Headers
X-RateLimit-Limit: 30
X-RateLimit-Remaining: 27
X-RateLimit-Reset: 41
Retry-After: 41        # only on 429