Most of the AI models developers use today are generative: you send a prompt, and they write something back. That's the right tool for writing a reply or a function. It's an awkward tool for the thousands of small decisions inside a data pipeline, where what you want isn't prose but a value your code can branch on.
This post looks at a different kind of model, applies it to five problems in onchain data, and walks through a full implementation of the first one.
Jev is a model from TypeSafe built for one job: making fast, typed decisions inside software. TypeSafe calls it a "System One" model, after the fast, intuitive mode of thinking in Kahneman's Thinking, Fast and Slow.
You don't prompt it. You send it state (a string, a JSON object or an array) and a set of named questions, each one of three types:
A request looks like this:
import { choice, noul, TypeSafeClient } from "@typesafe-ai/sdk";
const jev = new TypeSafeClient(); // reads TYPESAFE_API_KEY
const { answers } = await jev.systemOne({
state: { message: "I was charged twice for order A-104. Please refund the duplicate." },
questions: {
department: choice("Which team should handle `message`?", {
billing: "Charges, invoices, refunds",
technical: "Bugs and integration problems",
other: null,
}),
refund_requested: noul("Does `message` ask for money back?"),
},
});
answers.department.choice; // "billing" | "technical" | "other"
answers.department.confidence; // e.g. 0.93
answers.refund_requested.noul; // e.g. 0.97Four properties make it interesting for data work:
The output is typed by construction. A choice can only return one of the labels you defined, so there's no JSON to repair and no stray parenthetical to strip. In TypeScript, the SDK infers answer types from your questions, so answers.department.choice is a union of your labels.
Every answer comes with a confidence. TypeSafe trains Jev to produce calibrated probabilities, so you can branch on how sure the model is, not just on what it said. That turns a single model call into a routing decision: act on confident answers, escalate the rest.
It's cheap and fast. At the time of writing, Jev lists at $0.042 per million input tokens with free output, and TypeSafe quotes most calls at around 100 ms. Questions in a request run in parallel against the same state, so asking five questions costs little more than asking one.
It's deliberately narrow. Jev can't write text, can't explain itself, and doesn't do arithmetic, dates or counting reliably. Anything with an exact answer stays in your code.
That combination changes what's worth computing. Plenty of useful columns never got built because their value is a judgment: rules were too brittle, a trained model needed an ML team, and an LLM was too slow and expensive to run on every row. Astronomer's write-up makes this case for data engineering in general. We wanted to see how it holds up on blockchain data.
Onchain data is full of these judgments. The raw facts are all public and GoldRush serves them decoded, but the labels people actually want are interpretations of those facts. Here are five we think fit Jev well:
Wallet persona. Label every address by who likely operates it: exchange hot wallet, MEV bot, market maker, bridge or protocol, DAO treasury, retail user, or drainer. Compliance, security and analytics teams all want this, and it's the one we build end to end below.
Spam and scam token flags. Catch the long tail of airdropped phishing tokens and fake stablecoins that curated blocklists haven't caught up with yet.
Transaction intent. Classify what a transaction actually did (swap, bridge, LP change, claim, approval drain) from its decoded event sequence, and flag the malicious ones on a live stream.
Entity resolution. Decide whether two addresses or labels from different sources refer to the same entity, after cheap deterministic rules have narrowed the candidates.
Launch triage. For every new DEX pair, decide whether the token is a copycat of an existing one and what kind of launch it looks like.
All five share one shape: GoldRush supplies the data, code computes the facts, Jev makes one narrow typed judgment, and confidence decides what happens next.
The single most important design rule: Jev never sees a number it has to reason about. It doesn't do math, dates or counting reliably, and it's not supposed to. So the pipeline splits work cleanly:
Everything exact stays in code. Jev gets one short, readable description of each item (a wallet, a transaction, a token) and answers a handful of questions about it. Code decides what to do with the answer.
The persona column is the one every onchain team asks for. Is this address an exchange hot wallet, an MEV bot, a DAO treasury, a person, or a drainer? Compliance teams want it for risk scoring, security teams want it to decide which alerts matter, and product teams want it to keep a Binance hot wallet off a "top traders" leaderboard. Here's the full build, in about 400 lines of TypeScript.
Four endpoints give us enough signal to judge most wallets:
We call GoldRush with plain fetch. Every endpoint returns the same envelope, { data: { items }, error, error_message }, so one helper covers all four:
const GOLDRUSH_BASE = "https://api.covalenthq.com/v1";
async function goldrush<T>(path: string, params: Record<string, string> = {}): Promise<T[]> {
const qs = new URLSearchParams(params).toString();
const url = `${GOLDRUSH_BASE}${path}${qs ? `?${qs}` : ""}`;
const body = await httpJson<GREnvelope<T>>(
url,
{ headers: { Authorization: `Bearer ${process.env.GOLDRUSH_API_KEY}` } },
`GoldRush ${path}`,
);
if (body.error) throw new Error(`GoldRush ${path}: ${body.error_message}`);
return body.data?.items ?? [];
}
async function fetchWallet(address: string) {
const [txs, summary, balances, chains] = await Promise.all([
goldrush<GRTx>(`/${CHAIN}/address/${address}/transactions_v3/`, { "quote-currency": "USD" }),
goldrush<GRSummary>(`/${CHAIN}/address/${address}/transactions_summary/`),
goldrush<GRBalance>(`/${CHAIN}/address/${address}/balances_v2/`, { "quote-currency": "USD" }),
goldrush<GRChainActivity>(`/address/${address}/activity/`),
]);
return { txs, summary: summary[0] ?? null, balances, chains };
}Two details worth knowing:
Page 0 of transactions_v3 is the oldest page, not the newest. Calling the endpoint without a page number returns the most recent page, which is what you want for behavioral features. It's an easy mistake to make, and it'll quietly give you a wallet's behavior from 2019.
httpJson retries 429s, 5xx errors and network failures with exponential backoff and honors Retry-After. At four calls per wallet, GoldRush rate limits, not Jev, are what bound your throughput. Size your concurrency accordingly.
We only type the fields we read. Raw JSON gives timestamps as ISO strings and token amounts as strings, which suits us since we never do arithmetic on raw token amounts:
type GRTx = {
block_signed_at: string;
successful: boolean | null;
from_address: string | null;
from_address_label: string | null;
to_address: string | null;
to_address_label: string | null;
value_quote: number | null;
log_events: Array<{ decoded: { name: string } | null }> | null;
};Before we get into feature engineering, here's a quick look at the Jev dashboard running the wallet analysis end to end.
This is where the arithmetic lives. From the four responses we compute the features that separate personas in practice:
The implementation is ordinary TypeScript. A representative slice:
const me = address.toLowerCase();
const sent = txs.filter((t) => t.from_address?.toLowerCase() === me);
const sentTimes = sent.map((t) => Date.parse(t.block_signed_at)).sort((a, b) => a - b);
const gaps = sentTimes.slice(1).map((t, i) => (t - sentTimes[i]) / 1000);
const activeHours = new Set(txs.map((t) => new Date(t.block_signed_at).getUTCHours())).size;
const counterparties = new Map<string, number>();
for (const t of txs) {
const outgoing = t.from_address?.toLowerCase() === me;
const other = outgoing ? t.to_address : t.from_address;
const label = outgoing ? t.to_address_label : t.from_address_label;
if (!other) continue;
const key = label ? `${label} (${other.slice(0, 8)}…)` : `${other.slice(0, 8)}…`;
counterparties.set(key, (counterparties.get(key) ?? 0) + 1);
}Note that GoldRush's *_address_label fields do real work here. "Binance 14" in a counterparty list is a much stronger signal than an anonymous hex string, and it costs nothing extra.
The profile is the only thing Jev sees. We render features as short sentences with human-readable units, and never pass raw hex, wei or epoch timestamps:
const lines = [
`Chain analysed: ${CHAIN}. Active on ${chains.length} chain(s): ${chainNames}.`,
`Lifetime: ${lifetime.toLocaleString()} transactions over ${ageDays} days (first seen ${first}, last seen ${last}).`,
`Recent sample: ${txs.length} transactions over ${fmtGap(windowSeconds)}, about ${txPerDay} per day.`,
`${sentPct}% of recent transactions were sent by this address; ${failedPct}% failed.`,
`Median time between transactions it sent: ${fmtGap(medianGap)}.`,
windowDays >= 2 ? `Activity seen in ${activeHours} of 24 UTC hours.` : "",
`${counterparties.size} distinct counterparties in the sample. Top: ${top5}.`,
`Most common decoded events: ${topEvents || "none (plain transfers)"}.`,
`Holdings: ${fmtUsd(totalUsd)} across ${held.length} tokens, ${stablePct}% in stablecoins. Top: ${topHoldings}.`,
].filter(Boolean);Here's an illustrative profile for a high-frequency trading address (synthetic data, but this is the exact format the model receives):
Chain analysed: eth-mainnet. Active on 2 chain(s): eth-mainnet, base-mainnet.
Lifetime: 120,000 transactions over 400 days (first seen 2025-08-19, last seen 2026-09-23).
Recent sample: 50 transactions over 60 minutes, about 1200 per day.
66% of recent transactions were sent by this address; 16% failed.
Median time between transactions it sent: 60 seconds.
2 distinct counterparties in the sample. Top: Uniswap V3: Router (0xCCCccc…) x33; 0xBBBbbb… x17.
Most common decoded events: Swap x50, Transfer x50.
Median transaction value: $1.2K.
Holdings: $7.0M across 2 tokens, 71% in stablecoins. Top: USDC $5.0M, WETH $2.0M. 1 spam tokens received.One subtle bug we caught while building this: transactions_v3 returns roughly the last 100 transactions. For a busy exchange wallet, that can cover just a few minutes. If you print "activity seen in 2 of 24 UTC hours" for that wallet, you've told the model it looks like a human who's only awake in the afternoon. The profile only includes the hours line when the sample spans at least two days. Pay attention to what your features claim, not just what they compute.
A profile like this runs to a few hundred tokens. At Jev's list price, a million wallets costs on the order of $15–20 in model spend.
All questions and thresholds live in one block at the top of the file. That block is what you review in a PR and what you'll iterate on:
import { choice, noul, score, TypeSafeClient } from "@typesafe-ai/sdk";
const QUESTIONS = {
persona: choice(
"Based on `profile`, which kind of operator most likely controls this blockchain address?",
{
cex_hot_wallet:
"A centralized exchange or custodian wallet: very high volume, thousands of distinct counterparties, mostly plain token transfers in and out, little or no DeFi interaction.",
mev_bot:
"An MEV or arbitrage bot: mostly swap events across DEX pools, tight timing between transactions, activity spread across all hours, often a high share of failed transactions.",
market_maker:
"A professional trading or market-making firm: large-value swaps and transfers with a small set of venues and exchanges, steady daily activity, large stablecoin holdings.",
bridge_or_protocol:
"A bridge, protocol contract or router: receives far more transactions than it sends, interacts with many unrelated users, events are protocol-specific (deposits, withdrawals, mints).",
dao_treasury:
"A DAO treasury, multisig or timelock: few, infrequent transactions, governance or execution events, large holdings of a project's own token.",
retail:
"An individual person: modest activity, irregular human timing, a mix of transfers, swaps, NFT or app interactions, a small number of counterparties.",
drainer_or_scam:
"A phishing drainer or scam wallet: receives tokens via transferFrom or approvals from many unrelated victims, quickly moves funds out or swaps them to ETH or stablecoins.",
unclear: "The profile does not contain enough evidence to tell.",
},
),
automation: score("How automated is the activity described in `profile`?", [
"Clearly human: irregular gaps of hours or days, activity in a few hours of the day only.",
"Mostly human with some scripted or repeated actions.",
"Mostly automated: regular cadence, activity in most hours of the day.",
"Fully automated: many transactions per hour, seconds between transactions, around the clock.",
]),
interacts_with_known_exchange: noul(
"Does `profile` list a centralized exchange (for example Binance, Coinbase, Kraken, OKX) among the top counterparties?",
),
};
const ACCEPT_AT = 0.8; // label goes straight to the warehouse
const REVIEW_AT = 0.5; // between the two: second opinion from a larger model; below: a humanA few principles shaped these:
Describe situations, not degrees. Each persona is a description of observable behavior that maps to the features in the profile, not a bare label. "Tight timing between transactions, activity spread across all hours" gives the model something to match. "Bot" doesn't.
Always provide an exit. A choice must pick something. Without unclear, a wallet with three transactions gets forced into whichever persona is least wrong, with misleading confidence.
One dimension per question. Automation is its own score rather than being folded into the persona descriptions. A market maker and an MEV bot can both be fully automated. Keeping that axis separate lets you use it downstream, for example to exclude bots from a leaderboard regardless of persona.
Ask everything in one call. Jev evaluates questions independently against the same state, so the state is sent once and extra questions only add their own tokens. We ask the exchange-counterparty question for every wallet even though only some downstream consumers use it.
Point at fields. Backticked references like profile tell the model exactly which part of the state the question is about.
In TypeScript, the SDK infers answer types from the question definitions. res.answers.persona.choice is typed as the union of your eight labels, so a typo in a downstream switch is a compile error, not a silent null in a dashboard.
const jev = new TypeSafeClient({ defaultModel: "jev-1.13.0" }); // reads TYPESAFE_API_KEY
function route(confidence: number) {
if (confidence >= ACCEPT_AT) return "accept";
if (confidence >= REVIEW_AT) return "llm_review";
return "human_queue";
}
async function classify(address: string) {
const data = await fetchWallet(address);
const { features, profile } = buildProfile(address, data);
const res = await jev.systemOne({ state: { profile }, questions: QUESTIONS });
const { persona, automation, interacts_with_known_exchange } = res.answers;
return {
address,
persona: persona.choice,
persona_confidence: persona.confidence,
persona_probabilities: persona.probabilities,
automation_score: automation.score, // 0 = human … 3 = fully automated
cex_counterparty_prob: interacts_with_known_exchange.noul,
decision: route(persona.confidence),
model: res.model, // log the exact version that answered
input_tokens: res.usage.input_tokens,
features,
profile,
};
}The routing is the point of the whole exercise. Most wallets clear the bar and get labeled directly. The middle band goes to a larger model with more context, maybe the full transaction history. Only the genuinely ambiguous tail reaches an analyst, and every analyst decision becomes a new labeled example.
We persist persona_probabilities, not just the winning label. When a wallet splits 0.55 / 0.40 between market_maker and mev_bot, that's useful information for downstream risk scoring, and it's gone if you only store the argmax.
We also pin the model version (jev-1.13.0) and log res.model. A confidence threshold is only valid for the model it was tuned against.
A typed answer is not a correct answer. Jev can't return an invalid label, but it can return a valid wrong one. So the script accepts a labeled CSV and reports how often it agrees with your labels at each confidence band:
const band = (lo: number, hi: number) => {
const b = labelled.filter((r) => r.persona_confidence >= lo && r.persona_confidence < hi);
return b.length ? `${b.filter((r) => r.persona === r.expected).length}/${b.length}` : "-";
};
console.log(
`Agreement by confidence: <0.5: ${band(0, 0.5)} 0.5–0.8: ${band(0.5, 0.8)} ≥0.8: ${band(0.8, 1.01)}`,
);If the ≥0.8 band isn't close to perfect on your labels, raise ACCEPT_AT or rewrite the persona descriptions. Most of the tuning happens in the criteria text, not in thresholds. The workflow is:
Label 100–200 wallets you already know: exchange wallets, known bots, your own team's multisigs.
Run the script, read the disagreements, and rewrite the criteria that caused them.
Set thresholds from the agreement curve on your data, not from anyone's benchmark, including this post.
Keep the labeled set in the repo and rerun it whenever you change a question, a threshold, or the model version.
npm install
cp .env.example .env # GOLDRUSH_API_KEY, TYPESAFE_API_KEY
npm start # four well-known labeled mainnet wallets
npm start -- 0xabc... 0xdef... # your own addresses
npm start -- --file wallets.csv --chain base-mainnet
npm start -- --dry-run # build profiles only; no Jev calls--dry-run is worth using first. Read a dozen profiles yourself before sending any to a model. If a profile doesn't let you tell a market maker from an MEV bot, no model will either.
It's behavioral, not attributional. It tells you an address behaves like an exchange hot wallet, not which exchange owns it. Attribution still needs clustering and ground-truth labels.
It judges a recent window. Wallets change behavior. For production, recompute on a schedule and keep history rather than overwriting.
State can be adversarial. Token names and labels are attacker-controlled text. Treat any field an attacker can write as untrusted input, and test with hostile examples before you rely on the output for enforcement.
The remaining four use cases follow the same pattern as the persona column. Here they are as TypeScript sketches rather than full implementations.
All four assume a goldrush() REST helper like the one above, a jev client.
The problem: wallets fill up with airdropped phishing tokens, fake stablecoins and junk NFTs. balances_v2 already returns an is_spam flag, but thousands of new mints appear every day and curated lists always lag.
The shape: keep the existing flag as the baseline, and ask Jev only about tokens the baseline hasn't decided. Holder concentration and airdrop patterns are computed in code.
const TOKEN_RISK = {
is_spam: noul("Is the token described in `token` unsolicited spam or a phishing lure?", {
true: "Name or metadata contains a URL, a claim/reward call to action, or the token was airdropped to many wallets that never interacted with it.",
false: "An ordinary token that holders acquired deliberately.",
}),
impersonates_known_asset: noul(
"Does `token` imitate a well-known asset (for example USDC, USDT, WETH) by name or symbol without being it?",
),
};
for (const bal of await goldrush<GRBalance>(`/${chain}/address/${wallet}/balances_v2/`)) {
if (bal.is_spam !== null) continue; // baseline already decided
const holders = await goldrush<GRHolder>(`/${chain}/tokens/${bal.contract_address}/token_holders_v2/`);
const h = holderStats(holders); // code: count, top-holder share, share that never transferred
const { answers } = await jev.systemOne({
state: {
token: `Name: ${bal.contract_name}. Symbol: ${bal.contract_ticker_symbol}. ` +
`${h.count} holders; top holder owns ${h.topPct}%; ${h.neverTransferredPct}% of holders never moved it.`,
},
questions: TOKEN_RISK,
});
if (answers.is_spam.noul > 0.95 || answers.is_spam.noul < 0.05) writeFlag(bal, answers.is_spam.noul > 0.5);
else escalate(bal, answers); // ambiguous: review queue
}Without Jev: blocklists plus heuristics, such as a URL in the name, zero liquidity, or a holder count far above the transfer count. Precise for known patterns, but always one campaign behind.
Who uses it: wallets, portfolio and tax trackers, NFT marketplaces, explorers, and swap UIs.
How it helps users: spam is auto-hidden when the confidence is high and badged when it's medium, so real tokens rarely disappear. Fake tokens stop inflating net-worth totals and tax exports. A warning appears before anyone swaps into a token that impersonates USDC or WETH.
The problem: security teams need to know what a transaction did: a swap, a bridge, an LP deposit, a claim, or an approval drain. Decoded logs give you the events; intent is a judgment about the sequence. Hand-written rules per protocol don't cover protocols that launched last week.
The shape: subscribe to wallet activity, render the decoded event sequence as text, classify intent and maliciousness in one call, and route on confidence with a higher bar because a false negative is expensive.
const TX_INTENT = {
intent: choice("What did the transaction in `tx` accomplish?", {
swap: "Exchanged one token for another through a DEX or aggregator.",
bridge: "Moved assets to or from another chain.",
lp_change: "Added or removed liquidity from a pool.",
claim: "Claimed rewards, an airdrop or vested tokens.",
approval: "Only granted or revoked a token allowance.",
drain: "Moved a victim's tokens to a third party using a prior approval or permit.",
other: null,
}),
looks_malicious: noul("Does `tx` look like theft, phishing or an exploit?"),
};
stream(`subscription { walletTxs(chain_name: $chain, wallet_addresses: $watchlist) { tx_hash } }`,
async ({ tx_hash }) => {
const [full] = await goldrush<GRTx>(`/${chain}/transaction_v2/${tx_hash}/`);
const events = full.log_events.map((e) => `${e.decoded?.name}(${summarizeParams(e)})`).join(" -> ");
const ctx = await recipientContext(full); // code: recipient age, prior tx count, label if any
const { answers } = await jev.systemOne({
state: { tx: `Events: ${events}. Recipient: ${ctx}.` },
questions: TX_INTENT,
});
const p = answers.looks_malicious.noul;
if (p > 0.95) alert(full, answers);
else if (p > 0.3) llmReviewQueue.push({ full, answers }); // cheap first pass, expensive second
else log(full, answers);
if (answers.intent.choice === "drain") {
const approvals = await goldrush(`/${chain}/approvals/${full.from_address}/`);
notifyVictim(full.from_address, approvals); // show which allowances to revoke
}
});Without Jev: per-protocol classifiers keyed on topic hashes and function selectors. Precise for known protocols, blind to new ones, and a maintenance burden that grows with every deployment.
Who uses it: wallets, security platforms, exchanges, tax tools, and analytics dashboards.
How it helps users: activity history reads as plain actions, like "Swapped 1.2 ETH for USDC" instead of a list of hashes. Drain alerts link straight to the approval to revoke. Deposits from a suspicious source go to review before they're credited. Tax categories come pre-filled, and users confirm only the unclear ones.
The problem: is "Binance 14" the same entity as "Binance: Hot Wallet 20" in another vendor's feed? Is this USDC-looking contract on Base the canonical deployment? Label sources never join cleanly, and exact matching misses most of the real matches.
The shape: SQL is good at narrowing candidates and bad at judging them; Jev is the reverse. Generate candidate pairs deterministically, then ask one precise question per pair. The precision of the question matters more than anything else here, so write down what counts as proof.
const RULE = `Treat A and B as the same entity only if they share a deployer address or bytecode hash,
AND their token symbol and decimals match, AND they share at least one of their top-10 counterparties.`;
const SAME_ENTITY = {
same_entity: noul(`Following \`rule\`, are \`a\` and \`b\` the same entity?`),
};
// Step 1: deterministic candidate generation (cheap, exact)
const pairs = await sql`
SELECT a.addr AS a, b.addr AS b FROM labels a JOIN labels b
ON a.deployer = b.deployer OR a.bytecode_hash = b.bytecode_hash OR a.symbol = b.symbol
WHERE a.source <> b.source`;
// Step 2: Jev judges each pair
for (const { a, b } of pairs) {
const [ha, hb] = await Promise.all([
goldrush(`/allchains/address/${a}/balances/`),
goldrush(`/allchains/address/${b}/balances/`),
]);
const { answers } = await jev.systemOne({
state: { rule: RULE, a: describe(a, ha), b: describe(b, hb) },
questions: SAME_ENTITY,
});
const p = answers.same_entity.noul;
if (p > 0.95) merge(a, b);
else if (p > 0.2) reviewQueue.push({ a, b, p }); // never auto-merge on a maybe
}
// Guardrail: re-score a labeled pair set whenever RULE changes
assert((await accuracy(labeledPairs, RULE)) >= 0.95);Without Jev: exact matching on deployer, CREATE2 salt or bytecode hash, plus fuzzy string distance on labels, with a human team working through everything else.
Who uses it: compliance vendors, exchanges doing counterparty checks, treasury tools, and token aggregators.
How it helps users: one entity view across chains and label sources, with confident merges applied automatically and uncertain ones suggested for review. Multichain tokens link to their canonical deployment on each chain. Every merge keeps its confidence score, so an attribution can be defended later.
The problem: thousands of pools launch every day. Traders and risk teams want to know which launches are copycats of existing tokens, which look like rug setups, and which are organic. Levenshtein distance catches PEPE vs PEPE2, but not a deliberately confusing Unicode homoglyph or a "Baby" prefix.
The shape: for every new pair on the stream, look up similar existing tokens and the deployer's history in code, then ask whether this launch imitates one of the candidates. Jev is picking from a list of candidates you give it, not generating one, which is exactly what it's good at.
const LAUNCH = {
copycat_of_existing: noul(
"Does the token in `launch` imitate one of the tokens listed in `lookalikes` by name, symbol or branding?",
),
launch_type: choice("What kind of launch is `launch`?", {
meme: "A novelty or community token with no product claims.",
impersonation: "Borrows the name or branding of an existing project or brand.",
project: "Associated with a described product, protocol or team.",
unclear: null,
}),
};
stream(`subscription { newPairs(chain_name: SOLANA_MAINNET) { pair_address exchange base_token { name symbol address } liquidity } }`,
async (pair) => {
const lookalikes = await gqlQuery(`query { searchToken(query: $q) { name symbol } }`, { q: pair.base_token.symbol });
const dev = await deployerStats(pair); // code: prior launches, how many lost >90% in 24h
const { answers } = await jev.systemOne({
state: {
launch: `${pair.base_token.name} (${pair.base_token.symbol}) on ${pair.exchange}, ` +
`initial liquidity ${fmtUsd(pair.liquidity)}. Deployer launched ${dev.prior} tokens; ${dev.rugged} collapsed within 24h.`,
lookalikes: lookalikes.slice(0, 5).map((t) => `${t.name} (${t.symbol})`),
},
questions: LAUNCH,
});
publish("launch_feed", {
pair,
launch_type: answers.launch_type.choice,
confidence: answers.launch_type.confidence,
copycat: answers.copycat_of_existing.noul > 0.9,
});
});Without Jev: string similarity on name and symbol plus deployer heuristics. Fast, but easy to evade on purpose.
Who uses it: trading terminals, wallets with built-in swaps, launchpads, alert bots, and project teams watching for copies of their token.
How it helps users: the new-pairs firehose becomes a labeled feed, with badges like "copycat of X" or "deployer has rugged before." Users can filter launches to their own risk level. Copycats are warned about at buy time, and projects get alerted when someone launches a token under their name.
The common thread across all five use cases:
GoldRush does the data work. Decoded events, labels, balances, holders and cross-chain footprint, over REST or a stream. Jev is only as good as the facts you give it, and turning raw chain data into readable facts is the hard part.
Code does anything exact. Counting, dates, USD values, thresholds. If an answer has a right number, compute it.
Jev does the judgment. One narrow, typed question at a time, with criteria that describe what you can observe.
Confidence routes the work. Accept the confident answers, escalate the middle, and send the tail to a human. Store the probabilities, pin the model version, and keep a labeled set in the repo.
The economics are the part we find most interesting. When a judgment costs a small fraction of a cent, you stop rationing it. The interesting question is no longer how to make your three existing labels cheaper. It's which of the hundreds of labels you never bothered to compute are now worth having: persona on every address, intent on every transaction, is_copycat on every launch.