Documentation
How the numbers on this site are produced, what the dataset covers, and where it falls short. Written so the figures can be checked rather than taken on trust. Every count was queried from the production database on 2026-08-16.
Methodology
This section describes how KarmaScan collects data, scores it, and turns it into the numbers shown on the site. Every figure below was produced by reading the code in this repository or by running a read-only query against the production database on 2026-08-16. Where a mechanism has a known limitation, it is stated rather than omitted.
1. Where the data comes from
Source. Current ingestion reads from the Arctic Shift archive (https://arctic-shift.photon-reddit.com/api), using its posts/search and comments/search endpoints. KarmaScan no longer scrapes Reddit directly. Direct scraping was retired on 2026-08-13 after Reddit began returning HTTP 403 for anonymous .json endpoints on 2026-02-13. That block produced empty runs for roughly six months without failing, because the fetch layer turned a block into an empty result and the job still exited cleanly.
The stored corpus is a mix of both pipelines. Rows are separable by a write-time signature: the archive ingester stamps every comment content_type = 'text', while the old scraper left it blank or null. By that test, 309,148 rows (2025-07-21 through 2026-01-03, 51.9%) came from the direct scraper and 285,970 rows (2026-01-05 onward, 48.1%) came from Arctic Shift. Several rules described below are enforced by the current ingester and are not retroactively true of the older rows; each such case is flagged where it arises.
Before the switch, the archive was checked against the 283 overlapping records the old scraper had already collected for the same window. Every score matched exactly, and the archive returned more records than the scraper had captured. This comparison is recorded in the workflow file; the comparison script and its output are not in the repository, so the check cannot be re-run from the code alone.
Hourly-window pagination. A day is fetched in 24 one-hour slices, one slice at a time, sorted ascending by created_utc, 100 records per page (the API returns HTTP 400 above 100). Each slice is paginated to exhaustion, deduplicated by record id, with the cursor advanced to last_created_utc + 1.
The hourly slicing is not a performance choice. A single wide window that is paginated deeply returns HTTP 422 ("Timeout. Maybe slow down a bit"), which silently truncated the output of the earlier ingestion script. Slicing keeps every window shallow enough to complete. The client sleeps 0.45 seconds after each successful request and retries up to 6 times with doubling backoff (starting at 1.5 seconds) on 422, 429, 500, 502, 503, and 504.
Schedule and scope. A GitHub Actions workflow (.github/workflows/scrape.yml) runs on cron 0 6,18 * * *, that is 06:00 and 18:00 UTC daily, and can also be triggered manually. Each run ingests the last 3 complete UTC days, ending yesterday. Today is excluded on purpose: the current UTC day is still accumulating, and recording a partial day as complete would leave a permanent hole.
The workflow passes no --subreddit argument, so the ingester runs against its default, r/CryptoCurrency. Ongoing collection covers that one subreddit.
The 3-day window overlaps previous runs deliberately. Reddit scores keep moving for roughly 24 to 48 hours after posting (the reason recorded in the workflow), so each run re-upserts the window and refreshes scores collected earlier. A run fails if it writes zero rows, or if the newest created_date in the database is more than 3 days old. That second check exists because the 2026-02-13 stall was invisible for months when a clean exit was treated as a healthy run.
This is a batch pipeline. There is no minute-level or streaming update, and the newest data is normally one to two days behind the present.
What a row is. One row in reddit_activity is one Reddit post or one comment, keyed by Reddit's own id (so re-ingesting the same item updates it rather than duplicating it). Each row carries:
- author, subreddit, permalinks, and for comments the parent post id
created_date(UTC,YYYY-MM-DD) andmoon_week(the epoch number, see section 4)score(Reddit's score as seen at fetch time) andadjusted_scorepost_flair_type,post_type(postorcomment),content_typemod_distinguishedandrewards_exemptflagssentiment_raw,sentiment_adjusted,sentiment_label(section 2)- project attribution as JSON: the projects named, each project's share of the row's karma, and each project's sentiment (section 3)
The content classifier emits only text, link, or image. Posts are the only rows it runs on; the ingester writes text for every comment. A fourth value, other, appears on 288 posts, and blank or null values appear on 300,901 comments. All of those are legacy scraper rows dated 2026-01-03 or earlier.
adjusted_score differs from score in one case: posts flaired as memes are multiplied by 0.0025 and rounded, so meme karma cannot dominate karma-weighted aggregates. Comments never take that penalty, and the corpus bears this out: all 8,442 meme-flaired comments have adjusted_score = score. Of 1,147 meme-flaired posts, 887 match round(score * 0.0025). The 260 that do not are all dated 2026-01-03 or earlier and carry a roughly 0.25 multiplier from the old scraper. Every one of the 851 meme posts written by the current ingester follows the documented rule.
Two bot accounts (iowxss6_bot, coinfeeds-bot) are dropped at ingest by the current pipeline. 3,604 rows from those accounts survive from before that filter existed (coinfeeds-bot 3,178, ioWxss6_bot 426), the most recent dated 2026-01-03.
Content from [deleted] authors and AutoModerator is kept but flagged rewards_exempt. This flag is also only reliable on current-pipeline rows: 1,804 AutoModerator rows, all legacy, are not flagged, and the legacy data additionally flags two author names the current rule does not cover (deleted, 4,571 rows, and MoonWeek, 12 rows).
Corpus as it stands (queried 2026-08-16).
| Measure | Value |
|---|---|
Rows in reddit_activity |
595,118 |
| Distinct authors | 93,472 |
| Date range | 2025-07-21 to 2026-08-14 |
| Moon epochs covered | 14 (epochs 65 to 78) |
| Comments / posts | 553,201 / 41,917 |
| Project mention rows | 208,097 |
| Projects in the registry | 128 (127 have at least one attributed mention) |
Subreddit split: r/CryptoCurrency 591,073 rows (99.3%), r/CryptoMarkets 3,400, r/defi 461, r/WallStreetBetsCrypto 184. The three smaller subreddits are not an ongoing feed: every row from them falls between 2026-01-22 and 2026-02-03. Outside that 13-day window this is an r/CryptoCurrency dataset.
Coverage gaps. The range spans 390 calendar days and contains 373 days with data. The 17 missing days are 2026-01-04 and the block from 2026-02-14 to 2026-03-01, which is the start of the Reddit 403 outage. Those days are absent, not zero, and any per-day chart will show them as holes.
Recent daily volume, 2026-08-05 to 2026-08-14: 1,573 / 1,228 / 995 / 998 / 1,055 / 1,056 / 1,044 / 681 / 1,150 / 1,417 rows, averaging about 1,120 rows per day. Historical days average higher (595,118 rows over 373 days is about 1,596 per day), so recent capture is below the long-run average.
2. How sentiment is scored
The primary scorer is an LLM, not a lexicon. Rows are scored 0 to 100 by claude-haiku-4-5 through the Anthropic Message Batches API, one request per row, with the row's text truncated to the first 700 characters and the answer constrained to a JSON schema {"score": integer}, then clamped to 0 to 100. Failed, expired, or unparseable results leave the existing score untouched rather than writing a guess.
The scorer only queues rows whose text is non-null and at least 5 characters long. 9,142 rows fall below that bar and keep whatever the local fallback engine assigned at ingest.
The rubric. This is the system prompt used, reproduced from scripts/score_sentiment_llm.py with the source's own line breaks:
Score the crypto-market sentiment the AUTHOR expresses, 0-100:
0-10 despair/certain collapse; 15-30 clearly bearish; 35-45 mild bearish lean;
48-52 genuinely neutral (pure info, logistics, no-lean questions);
55-65 mild bullish lean; 70-85 clearly bullish; 90-100 euphoria.
Rules: judge the author's stance, not events described or views they quote or
mock. Sarcasm inverts. Mocking bears is bullish; mocking bulls is bearish.
Crypto slang carries signal (wagmi/dca and chill = bullish; exit liquidity/
rugged = bearish; rocket and moon emoji bullish). Bot/template text
("I am a bot...") is exactly 50. News headlines and article shares WITHOUT author commentary are 48-52 regardless of how positive or negative the news itself is; the author reporting news is not the author taking a stance. Price predictions count: an explicit target
far above spot is bullish even with no emotional words.
What the scale means. 0 is certainty of collapse, 100 is euphoria, and 50 is the absence of a market view. 50 is not "mildly positive" or an average of opposing views. It is what a logistics question, a neutral explainer, or a bare link deserves.
The score is about the author, not the news. This is the property that matters most when reading the numbers. The model is asked what stance the writer takes, not whether the events described are good or bad. Four rulings follow from that, and they are in the rubric on purpose:
- Sarcasm inverts. "Great, another 20% day, love this for us" is bearish, not bullish.
- Mocking bears reads bullish (and mocking bulls reads bearish). Ridiculing a position is taking the opposite one.
- Bot and template text is exactly 50. Moderator boilerplate carries alarming vocabulary ("scam", "abuse", "removed") but expresses no view. The local fallback engine hard-codes the same rule for four known template phrases.
- Sharing a bearish headline with no commentary is neutral (48 to 52). A user who posts ten crash headlines and says nothing is a news poster, not a bear. Before this ruling, that pattern labeled news posters "Very Bearish".
What the model supplies, and what stays deterministic. The LLM writes sentiment_raw only. Two derived fields are computed locally so their meaning does not drift when the scorer changes.
Karma adjustment:
centered = sentiment_raw - 50
multiplier m:
karma > 0 -> m = min(1 + 0.30 * log10(max(karma, 1)), 2.5)
-3 < karma<=0 -> m = 1.0
-10 < karma<=-3 -> m = 1.2
karma <= -10 -> m = 1.4
sign factor f:
karma <= -5 -> f = -1.0 (flip: the sub rejected the take)
-5 < karma < 0 -> f = 0.5 (dampen)
karma >= 0 -> f = 1.0
sentiment_adjusted = clamp(50 + centered * m * f, 0, 100), truncated to an integer
Label bands, applied to the raw score:
>= 75 very_bullish
>= 58 bullish
>= 42 neutral
>= 25 bearish
< 25 very_bearish
Note that the label's neutral band (42 to 58) is wider than the rubric's "genuinely neutral" band (48 to 52). The rubric governs the number; the labels are coarser buckets applied afterward.
The lexicon engine is a fallback only. app/core/sentiment.py combines VADER with a weighted crypto lexicon. It runs at ingest time so that a freshly written row is never blank, and it runs when no API key is configured. It is not what the site reports for scored rows. Rows the LLM has scored are recorded in a local ledger (.llm_sentiment_checkpoint.json), and both the daily ingest and the full-corpus rescan use that ledger to refresh karma and project attribution while leaving LLM sentiment in place. Without that guard, the daily 3-day re-upsert would overwrite good scores with fallback scores. Only rows that returned a usable score are added to the ledger.
As of 2026-08-16 the ledger holds 586,141 ids. 585,976 of them match a row currently in the table, so 98.5% of the 595,118 rows carry an LLM score. The remaining 165 ledger entries have no matching row. Read the other way, the coverage is exact: every one of the 585,976 rows eligible for scoring has been scored, and the 9,142 unscored rows are precisely those whose text is too short to submit.
Validation, stated plainly. Both engines were measured against the same blind set of 510 texts labeled by a human, in August 2026.
| Engine | MAE | Pearson r |
|---|---|---|
claude-haiku-4-5 with the rubric above |
5.17 | 0.834 |
| Local lexicon engine, current settings | 19.98 | not recorded |
| Local lexicon engine, before the Aug 2026 retune | 25.96 | not recorded |
MAE (mean absolute error) is the average distance between the engine's score and the human label on the 0 to 100 scale: 5.17 means the model is typically about 5 points away from the human. Pearson r measures whether the two move together, from -1 to 1: 0.834 means that when a human ranks one text as more bullish than another, the model usually agrees on the direction and roughly on the gap.
Three caveats a buyer should weigh. First, the 510-text labeled set and the scoring script that produced these figures are not stored in this repository and do not appear anywhere in its git history, so the numbers cannot be re-derived from the code alone; they come from the commit record (commit 606aba0, 2026-08-14), the README, and code comments written when the validation was run. Second, that commit describes the 5.17 and 0.834 result as a sample validation, not a full-corpus one. Third, the retune that produced the lexicon engine's 19.98 also removed a "stretch" transform that had been manufacturing confident extreme scores; on the labeled set, engine scores of 0 to 9 missed the truth by about 39 points and scores of 90 to 100 missed by about 42, so the fallback engine was made deliberately more conservative rather than more decisive. The 25.96 pre-retune figure comes from the commit message; a separate code comment records an alpha sweep in which the old stretch setting measured 25.71. The two numbers describe different comparisons and are not reconcilable from what is stored.
Distribution of the current corpus. Labels break down as neutral 293,167, bearish 128,928, bullish 94,585, very_bearish 45,346, very_bullish 33,092. 235,364 rows (39.46%) sit at exactly 50.
An earlier version of this page attributed that neutral mass to the rubric declining to read stance into news shares, questions, and logistics. A decomposition of those rows does not support that explanation, so here is what they actually are:
| Category | Rows | Share of the 50-mass |
|---|---|---|
| Deleted or removed placeholders with no text | 40,240 | 17.10% |
| Short real comments (under 40 chars) with no stance assigned | 72,254 | 30.70% |
| Real text of 40 chars or more | 77,884 | 33.09% |
| News or image shares | 14,866 | 6.31% |
| Bot or template text | 4,453 | 1.89% |
| Bare URLs | 1,301 | 0.55% |
| Remainder | 24,366 | 10.36% |
Only 8.76% of the neutral mass falls into the categories the rubric is designed to score as neutral. The two largest groups are a data-hygiene problem and a model limitation, not intended behaviour:
- Placeholders. 40,243 rows whose entire text is
[removed],[deleted], or a moderator removal notice were scored 50 and counted as neutral market sentiment. They carry no opinion and are being excluded from sentiment aggregates. - Short comments. 32.88% of the corpus is under 40 characters, and among rows under 20 characters, 72.97% scored exactly 50 against 26.07% for rows of 80 to 199 characters (measured 2026-08-16, before the rescore described below). Short Reddit comments carry meaning by convention and by what they reply to, and the text-only scorer does not see the parent post. Measured rather than estimated: across 1,641 short agreement or disagreement openers sitting under strongly polarized parents, 338 carried an inverted sign and 840 were flattened to neutral.
Thread-context rescoring (applied 2026-08-18). The addressable part of the short-comment problem has since been rescored with the parent post supplied as context. Strict eligibility gates decide which rows this applies to, because context helps exactly where the measurement said it does and can mislead elsewhere: the comment must be short (3 to 80 characters), previously near-neutral (45 to 55), readable (not a deletion placeholder or bare URL), and sitting under a parent whose own score is polarized (30 or below, 70 or above). 20,364 comments met the gates and were rescored deterministically (temperature 0) with the same rubric plus a context rule: agreement inherits the parent's direction, disagreement inverts it, and a comment that stands on its own is scored on its own text. Results, from the per-row audit log: 8,352 rows (41.0%) moved more than 3 points (5,931 to bearish, 1,380 to bullish, the rest within the mid-band), and 12,012 were confirmed unchanged. The bearish lean follows the input mix, not a scorer bias: 67.0% of eligible comments sat under bearish parents, and under bullish parents the moves split nearly evenly (1,117 bullish, 967 bearish). Rows scored this way carry sentiment_source = 'llm_ctx' so they are distinguishable from text-only scores, and every change is recorded with its before and after value.
The aggregates in sections 5 and 6 down-weight or exclude neutral rows rather than averaging them in, which limits how far this distorts published figures. It does not eliminate it.
3. How projects are detected in text
Detection decides which rows count toward a project. It is deliberately strict, and it favors missing a mention over inventing one.
Preparation. Text is lowercased, URLs are removed, and markdown link syntax is stripped.
Tokenization with clause boundaries. Text is split into clauses on . , ; : ! ? ( ) and newlines. Within a clause, tokens are runs of [A-Za-z0-9$#@']. A sentinel is inserted between clauses. A multi-word keyword must match adjacent tokens inside one clause, so a phrase cannot form across a comma. "coins cold, card payments are separate" does not register ColdCard, while "my cold card is a bitcoin only device" does. Hyphens split into adjacent tokens, so hyphenated spellings still match phrase keywords.
Matching. Single-word keywords require exact token equality, never substring containment. That is what stops "pol" matching "police", "sol" matching "sold", and "arb" matching "garbage". Multi-word keywords require an exact adjacent token sequence.
Longest-match-first span claiming. Within one project, keywords are tried longest phrase first, and matched token positions are claimed. A component word cannot re-count a mention its own phrase already covered, so "useless coin" counts once rather than once for the phrase and once for the bare word. This matters because mention counts drive each project's share of a row's karma, so a double count would inflate one project relative to the others in the same post. Claimed spans reset for each project, so two different projects can legitimately both count the same token.
Context gates. Some project names are ordinary English. Two per-keyword tables handle them, and both use the same strict token and phrase matcher:
- Exclusions are checked first. If any excluded term appears anywhere in the text, that keyword's hits are dropped outright.
- Required context. If a keyword has a required-context list, it counts only when at least one of those terms also appears in the text.
Gating is per keyword, not per project. "cryptopunks" is unambiguous and is never gated, while the bare word "punks" is. The registry currently holds 128 projects and 388 keyword entries (386 distinct strings, two of which are shared by more than one project) across 13 categories. 11 projects carry a required-context list and 11 carry an exclusion list, one gated keyword each.
Worked examples, all run against the live detector. Each arrow shows the complete set of projects the detector returned, not just the one under discussion:
ledger required context: wallet, hardware, nano, stax, flex, cold storage,
cold wallet, seed phrase, recovery phrase,
self custody, air gapped, ledger live
"The distributed ledger records every transaction." -> []
"Ledger just shipped a firmware update for the Nano." -> [Ledger]
limitless required context: prediction market, prediction markets,
kalshi, polymarket
"The upside here is limitless." -> []
"Limitless is my favourite prediction market now." -> [Limitless]
pol exact-token match; exclusions: "pol pot", "raoul"
"The police showed up at the conference." -> []
"Staking POL after the MATIC migration." -> [Polygon] (2 hits)
"Pol Pot was a dictator." -> []
compound required context: defi, lending, lend, borrow, comp, aave,
protocol, supply apy
exclusions: compound interest, compounding, compounded,
compound growth, i compound, compound my,
compound their, losses compound, compound over
"Compound interest is the eighth wonder of the world."-> []
"Supplying USDC on Compound, better rate than Aave." -> [USDC, Aave, Compound]
maker required context: dai, makerdao, mkr, vault, sky protocol,
cdp, stablecoin
exclusions: market maker, market makers, decision maker,
coffee maker, kingmaker, matchmaker, maker of
"That market maker is manipulating the DAI peg on
lending markets." -> [DAI]
"Maker vaults let you mint DAI against ETH." -> [Ethereum, DAI, MakerDAO]
The "market maker" case shows why both tables are needed. Required context alone cannot save it: the sentence contains "dai" and "lending", so MakerDAO's required context is satisfied. Because exclusions run first, the mention is still dropped. DAI, a separate tracked project, is still credited, which is correct.
The trade-offs, stated. These gates cost real mentions. A post that says only "Ledger" with no wallet or hardware vocabulary is not counted. A post that says only "Limitless" with no prediction-market vocabulary is not counted. That is the intended direction of the error: undercounting a project is recoverable, while counting every use of the word "ledger" is not.
The gates also work at the level of the whole text, not the span around the keyword. A wallet word anywhere in the row unlocks the keyword everywhere in that row, so "I keep my seed phrase off the ledger entirely" does register Ledger. Detection is keyword matching with guards, not language understanding.
What detection feeds. Detection decides which rows are attributed to a project. The sentiment shown for that project is then computed from those rows' scores as a karma-weighted, stance-weighted average (the stance weighting is defined in section 5).
4. Moon epochs
r/CryptoCurrency organizes activity into 28-day cycles. KarmaScan buckets every row into one of them at write time, from the row's UTC creation date, and stores it as moon_week. All 595,118 rows carry a numeric epoch.
The calculation is a fixed anchor plus floor division, with no lookup table:
epoch_length = 28 days
anchor = epoch 69 begins 2025-11-10
epoch(date) = 69 + floor((date - 2025-11-10) / 28 days)
epoch_start(n) = 2025-11-10 + (n - 69) * 28 days
epoch_end(n) = epoch_start(n) + 27 days (inclusive)
Floor division means dates before the anchor resolve backwards correctly. Worked examples from the running code:
- Epoch 65: 2025-07-21 to 2025-08-17 (the first day of the corpus is the first day of epoch 65)
- Epoch 71: 2026-01-05 to 2026-02-01
- Epoch 78: 2026-07-20 to 2026-08-16 (the epoch in progress on 2026-08-16)
The epoch selector never offers an epoch beyond the current one. A row carrying a future epoch indicates a bad upstream timestamp, and presenting it as a selectable range would put an empty period in front of a reader as though it were real.
5. The bullish/bearish index
What is actually served. The value on the gauge and at GET /api/v1/analytics/fear-greed (endpoint path kept for compatibility) is a two-term blend:
composite = W * market_index + (1 - W) * reddit_sentiment
W = COMPOSITE_MARKET_WEIGHT, default 0.6, clamped to [0, 1]
result = clamp(composite, 0, 100), rounded to 1 decimal
No environment file in this repository sets COMPOSITE_MARKET_WEIGHT, so the default applies unless the deployment environment overrides it. At that weight, the headline number is 60% an external market index and 40% Reddit. It is not a pure Reddit measure, and it should not be read as one.
The market term. market_index is the Crypto Fear and Greed Index published by Alternative.me (https://api.alternative.me/fng/?limit=1), fetched as an integer 0 to 100 and cached for one hour. On any failure the last successfully fetched value is reused, so one bad request does not move the gauge.
The Reddit term. reddit_sentiment is a karma-weighted and stance-weighted average of sentiment_adjusted over rows whose created_date is within 3 days of the current UTC date. Because ingest runs a day or two behind, that window holds less than three days of data. Evaluated on 2026-08-16, the cutoff was 2026-08-13 and the window contained 2,567 rows across two dates.
stance weight for row i:
w_i = max(|sentiment_adjusted_i - 50| / 50, 0.1)
reddit_sentiment = sum(sentiment_adjusted_i * |karma_i| * w_i)
/ sum(|karma_i| * w_i)
if the denominator is zero or null -> 50
Stance weighting exists because of the distribution shown in section 2: 39.5% of rows are scored exactly 50. A plain average would let that mass pull every reading toward the middle regardless of what the opinionated rows said. A row that takes a clear position (far from 50) carries up to full weight; near-neutral filler is damped to as little as one tenth. The 0.1 floor guarantees no row is silenced entirely. Karma is taken as an absolute value, because the sign flip for heavily downvoted content has already been applied inside sentiment_adjusted.
Fallback. If the market index has never been fetched successfully and nothing is cached, the endpoint returns the Reddit value alone rather than failing, and marks components.source as reddit_only instead of composite. The response always exposes its parts: the market value, the Reddit value, the weight in force, and the source flag.
Label bands. The index is labeled in market-stance terms rather than the Fear/Greed wording used by other sites; the bands are unchanged.
>= 75 Extreme Bullish
>= 55 Bullish
>= 35 Neutral
>= 15 Bearish
< 15 Extreme Bearish
Epoch average. When an epoch or epoch range is selected, the epoch average returned alongside the headline is the same stance-weighted, karma-weighted Reddit average over that period. It is Reddit only and is not blended with the market index.
A dead code path, disclosed. The database layer (app/services/database.py) also computes a four-term composite: 0.50 Reddit sentiment, 0.20 volume momentum, 0.10 sentiment dispersion, 0.20 external index, with a 0.60 / 0.25 / 0.15 fallback when the external index is unavailable. The endpoint does not serve that result. It reads only the Reddit component out of that function and recomputes the composite with the two-term formula above. Volume momentum and dispersion are calculated and discarded. The endpoint does contain a chain of fallbacks that would reach the four-term value if the Reddit component key were missing from the response, but that function always populates it, so no served number depends on the discarded terms in practice. They are documented here because the code contains them and a reader auditing the repository will find them.
A related detail: the two layers fetch the same Alternative.me index through separate functions with separate caches (one hour for the served path, fifteen minutes for the unused path).
6. Stance badges
A stance badge is the Bullish / Bearish / Neutral tag shown next to a contributor on the leaderboard. It is a ratio of side-taking statements, not an average score.
voiced statements = comments and text posts by that author
(link and image shares excluded)
bull = voiced statements with sentiment_raw >= 58
bear = voiced statements with sentiment_raw <= 42
stance_n = bull + bear (neutral statements excluded)
voiced_n = all voiced statements
if stance_n < 10 -> Neutral
bullShare = bull / stance_n
stanceRate = stance_n / voiced_n
damping = min(1, stanceRate / 0.25)
s = (2 * bullShare - 1) * damping
s >= 0.6 Very Bullish
s >= 0.1 Bullish
s <= -0.6 Very Bearish
s <= -0.1 Bearish
otherwise Neutral
s runs from -1 (every side-taking statement is bearish) to +1 (every one is bullish), with 0 meaning an even split. The damping term handles the author who takes a side three times and stays neutral five hundred times: without it, three bullish and one bearish statement would read Bullish.
The 0.25 threshold was chosen against the corpus. A code comment records the measured 10th-percentile stance rate as 40.3%. Re-derived on the corpus as it stands on 2026-08-16, the 10th percentile among the 4,699 authors who clear the 10-statement floor is 45.8%, and the median is 62.9%; the 40.3% figure does not reproduce, presumably having been measured on an earlier snapshot or a different author scope. Either figure supports the same conclusion, which is the only thing the threshold rests on: ordinary authors sit well above 0.25 and are never damped, so damping reaches only outliers. Of 93,472 authors in the corpus, 4,699 (5.0%) clear the 10-statement floor and can receive a non-default badge.
Three deliberate rulings shape this badge.
Voiced content only. Comments and text posts count. Link and image shares do not. A link share is something the author reports, not something the author says. Before this rule, a user who posted bearish headlines and wrote nothing was labeled "Very Bearish" on the strength of other people's headlines.
Counts, not karma. Every voiced statement counts once, at its raw score, with no karma weighting. Karma measures how much the subreddit applauded a post. It does not measure how strongly the author believes it. One post that earned 50,000 karma is one statement, and it must not outweigh a thousand quieter bullish comments from the same person. This is the opposite choice from the fear and greed index in section 5, and the difference is intentional: that index asks what the subreddit as a whole is expressing, where applause is part of the signal, while the badge asks what one person thinks.
Neutrals excluded from direction. Neutral statements carry no stance information, so they are removed from the numerator and denominator of bullShare. They still appear in voiced_n, which is how the damping term notices an author who rarely commits either way.
Minimum sample. Fewer than 10 side-taking statements returns Neutral. A user with two bullish comments does not get a Bullish badge.
Where else stance appears. Two other surfaces compute stance differently, and the numbers will not always agree:
- The user profile stance uses a plain unweighted average of
sentiment_rawover voiced content, bucketed at the same 75 / 58 / 42 / 25 thresholds as section 2 (its label strings arebullandbearrather thanbullishandbearish). It has no minimum sample and does not exclude neutrals. - The category KOL boards report
avg_sentimentas a karma-weighted and stance-weighted average (the same weighting as section 5), not as a bull share.
On the leaderboard response, the field named avg_sentiment carries the bull share as a percentage, not an average sentiment score, and it is reported as 50.0 for any author below the 10-statement minimum. One further disclosure about the same response: the total_score column has per-epoch karma adjustments from a server-side configuration table applied after aggregation, and the response carries no marker indicating that an author's figure was adjusted.
7. Verified members and account merges
Account merges. When one person has posted from more than one Reddit account, the accounts are merged into a single identity everywhere karma is aggregated or displayed: leaderboards, KOL boards, and user profiles. The merge is applied at aggregation time from a maintained alias list; the raw rows always keep the author Reddit actually recorded, so the underlying data never diverges from the source. Merges are added only on the operator's direction with evidence the accounts share an owner.
Verified members. Members who verify their humanity on an external reputation platform (currently Ethos) carry a karma multiplier that applies to both displayed karma and rankings. Verified rows are marked with a verified: true field in API responses. The multiplier, platform, and verification date are stored per member; measured (pre-multiplier) figures remain recoverable from the raw data since the multiplier is applied at aggregation time only.
Coverage and limitations
Every figure below was queried from the production database on 2026-08-16, around 12:15 UTC. This section exists so that a limitation turns up here rather than in your own diligence. Where something is broken, unfinished, or not yet backfilled, it is written that way.
What the corpus covers
KarmaScan reads Reddit and nothing else. As of the query date the activity table holds 595,118 rows (posts and comments) from 93,472 distinct authors.
| Subreddit | Rows | Share |
|---|---|---|
| r/CryptoCurrency | 591,073 | 99.3203% |
| r/CryptoMarkets | 3,400 | 0.5713% |
| r/defi | 461 | 0.0775% |
| r/WallStreetBetsCrypto | 184 | 0.0309% |
This is a single-community dataset. Over 99% of it is r/CryptoCurrency. The other three subreddits are not a second opinion on anything: all 4,045 of their rows fall inside a single 13-day window, 2026-01-22 to 2026-02-03, which straddles the boundary between stored epochs 71 and 72. r/CryptoMarkets and r/defi have data on all 13 of those days, r/WallStreetBetsCrypto on 10 of them. Nothing before or after. The ingestion job that runs today fetches r/CryptoCurrency only, because the --subreddit argument in scripts/backfill_arctic.py defaults to CryptoCurrency and the scheduled workflow does not override it. Treat r/CryptoMarkets, r/defi, and r/WallStreetBetsCrypto as a two-week historical sample that is not being extended.
Whether one large community is the right lens is your call. It is a deliberate focus, not an accident, but nothing in the product corrects for it.
Composition of the corpus:
- 553,201 comments and 41,917 posts.
- 128 tracked projects, all marked active, across 13 categories, with 208,097 attributed mentions. 127 of the 128 projects have at least one mention (Beam, in the Gaming category, has none).
Date range, epochs, and volume
The figures in this section were measured on 2026-08-16. A historical backfill of epochs 62 to 64 (2025-04-28 through 2025-07-20) is in progress as of 2026-08-18 and will extend the range below when complete; rows from those epochs carry lexicon scores until their LLM scoring pass runs.
- First day with data: 2025-07-21. Most recent day with data: 2026-08-14.
- That span is 390 calendar days; 373 of them have at least one row.
- 14 Moon epochs, numbered 65 to 78. Epochs are 28 days.
- Mean of 1,595 rows per day that has data. The daily range across the whole corpus runs from 9 to 6,370. Over the seven days ending 2026-08-14 the daily counts were 998, 1,055, 1,056, 1,044, 681, 1,150 and 1,417.
| Epoch | Rows | First day present | Last day present | Days with data |
|---|---|---|---|---|
| 65 | 61,971 | 2025-07-21 | 2025-08-16 | 27 |
| 66 | 35,993 | 2025-08-17 | 2025-09-13 | 28 |
| 67 | 51,680 | 2025-09-14 | 2025-10-11 | 28 |
| 68 | 64,372 | 2025-10-12 | 2025-11-08 | 28 |
| 69 | 56,124 | 2025-11-09 | 2025-12-06 | 28 |
| 70 | 39,008 | 2025-12-07 | 2026-01-03 | 28 |
| 71 | 32,980 | 2026-01-05 | 2026-02-01 | 28 |
| 72 | 30,189 | 2026-02-02 | 2026-02-13 | 12 |
| 73 | 32,332 | 2026-03-02 | 2026-03-29 | 28 |
| 74 | 33,476 | 2026-03-30 | 2026-04-26 | 28 |
| 75 | 32,456 | 2026-04-27 | 2026-05-24 | 28 |
| 76 | 53,942 | 2026-05-25 | 2026-06-21 | 28 |
| 77 | 41,722 | 2026-06-22 | 2026-07-19 | 28 |
| 78 | 28,873 | 2026-07-20 | 2026-08-14 | 26 |
Three epochs are not whole and should not be compared like for like against the rest:
- Epoch 65 is where the corpus begins. It holds 27 days. Every day inside its stored window is present, so it is short only against the 28-day window the code computes.
- Epoch 72 holds 12 of 28 days, or 43% of the cycle. See the gap below.
- Epoch 78 was still open on the query date. Its 28-day window closes on 2026-08-16 and the corpus held 26 of those days.
One labelling caveat. From epoch 71 onward, the epoch stamped on each row matches the boundaries computed by app/core/epochs.py (reference: epoch 69 begins 2025-11-10). For epochs 65 to 70, which were loaded from CSV imports, the stored boundaries run one day earlier than that formula: the stored start of each of epochs 66 to 70 is one day before the formula's start, and stored epoch 65 ends on 2025-08-16 where the formula ends it on 2025-08-17. Stored epoch 65's first day matches the formula only because the corpus begins on that date. If you recompute epoch membership from the code and compare against the stored moon_week values for 2025, expect a one-day disagreement at each boundary.
Data gaps
Seventeen calendar days between the first and last day of the corpus have no rows at all:
- 2026-01-04 (one day, between stored epochs 70 and 71)
- 2026-02-14 through 2026-03-01 (16 consecutive days, all inside epoch 72)
Cause of the 16-day hole. The workflow header records that Reddit began returning HTTP 403 for anonymous .json endpoints on 2026-02-13. The fetch layer converted a block into an empty result, so the scheduled job collected nothing and still exited zero. Nothing alerted. The last row the live scraper wrote is timestamped 2026-02-13 18:21 UTC, and the last day it covered is 2026-02-13, which matches. Ingestion was later rebuilt on the Arctic Shift archive instead of Reddit's own endpoints, and a recovery backfill restored the months that followed: the backfill checkpoint records 164 completed days from 2026-03-02 to 2026-08-12 and 220,234 rows written, which is exactly the row count the database holds for that date range.
The hole is still open. That recovery run started at 2026-03-02, by design, since it was written to recover epochs 73 to 78. The 2026-02-14 to 2026-03-01 window was never included and has not been backfilled since. It is not a hole in the archive: probing the Arctic Shift comments endpoint on 2026-08-16 returned records for 2026-02-14, 2026-02-20 and 2026-03-01, so the data appears recoverable. It has simply not been recovered yet.
Practical consequences:
- Epoch 72 totals (rows, karma, contributors, mentions) cover 12 of 28 days and will read low against any other epoch. Any epoch-over-epoch chart that includes epoch 72 is misleading unless you normalise for days.
project_mentionscontains zero rows dated inside the gap, so per-project trend lines break there too.- The single missing day 2026-01-04 sits at the boundary between the stored epoch 70 and epoch 71 windows. The archive returns records for that date as well; the reason it was never loaded is not established.
Thin days. Nine further days are present but sparse, well below the 1,595-row daily mean: 2025-08-25 (114 rows), 2025-08-26 (26), 2025-08-27 (9), 2025-08-28 (15), 2025-08-29 (10), 2025-09-16 (102), 2025-09-17 (54), 2026-01-14 (159) and 2026-01-21 (184). A day with 9 rows is a hole in everything but name.
Refresh cadence and lag
Ingestion runs on a GitHub Actions schedule, cron: '0 6,18 * * *', so twice a day at 06:00 and 18:00 UTC. It is the only workflow in the repo. Each run ingests three complete UTC days ending yesterday, then runs the LLM sentiment pass over the same window.
What that means for freshness:
- A post is only eligible for ingestion after its UTC day has closed. The earliest it can be picked up is the 06:00 UTC run the next morning. So the design floor is roughly 6 hours (for something posted just before midnight UTC) and roughly 30 hours (for something posted just after midnight UTC).
- The three-day window is deliberate overlap, not redundancy. The workflow header states that Reddit scores keep moving for about 24 to 48 hours, and each run re-upserts the window so karma refreshes. Once a row falls out of the three-day window, its karma is frozen at whatever it was last seen at.
- The run fails loudly if the newest row in the database is more than three days old (
--max-staleness-days 3), a direct response to the six months of silent empty runs described above.
Observed lag on the query date: at 2026-08-16 around 12:15 UTC, the newest day in the database was 2026-08-14 (1,417 rows). The 2026-08-16 06:00 UTC run should have covered 2026-08-15; that day was absent at query time, and the archive did return records for 2026-08-15 when probed at the same time, so the missing day is not an archive limitation. Plan for a lag of one to two days, not hours.
Sentiment is written twice per row. At ingest, the local engine scores it. The LLM pass in the same job then re-scores the same window with Claude Haiku and overwrites those values.
One checkpoint file, .llm_sentiment_checkpoint.json, records which row ids already carry LLM scores, and both scripts read it. The ingest step uses it to preserve existing LLM sentiment on re-upsert instead of writing local scores over it; the scorer would use it to skip finished work. Two things defeat that in practice. The file is gitignored and untracked, so in CI it is absent and the ingest step writes local-engine scores across the whole window. And the scorer reads it only when passed --resume, which the scheduled job does not pass, so the whole window is re-scored on every run whether or not the file exists. (The local copy currently lists 586,141 scored ids, against 585,976 rows that presently meet the scoring filter.)
If the LLM step fails or the job hits its 60-minute timeout, those rows stay on local-engine scores until a later run picks them up. Sentiment for the most recent three days is therefore best read as provisional.
There is no HTTP caching layer in front of the API: the FastAPI app sets no Cache-Control headers and vercel.json configures none. The only caches are in-process, and both sit on the same Alternative.me index: a 15-minute cache on the sentiment-calibration fetch and a one-hour cache on the composite Fear and Greed fetch.
Aggregation staleness
Two stored aggregate tables exist and both stopped being written in February 2026:
| Table | Rows | Coverage | Last computed |
|---|---|---|---|
project_metrics |
479 | epochs 65 to 72, 64 projects | 2026-02-03 18:49 UTC |
global_metrics |
176 | 2025-07-21 to 2026-02-03, epochs 65 to 72 | 2026-02-03 18:49 UTC |
They are written only by main.py, the legacy CSV import path, which the current scheduled job does not call. They are more than six months stale and cover exactly half the tracked projects (64 of 128).
The mitigating fact is that nothing reads them. Searching the application code, the only references to ProjectMetrics and GlobalMetrics outside the model and schema definitions are the write paths in app/services/database.py. No API endpoint reads either table. Every served surface (dashboard totals, epoch leaderboards, KOL boards, project pages, the Fear and Greed gauge and its 14-day history) queries reddit_activity and project_mentions at request time. So the stale tables are dead weight rather than a source of wrong numbers. They should be dropped or rebuilt; until then, ignore them if you are handed a database dump.
Three related emptinesses, since they will show up in a schema review: trending_content, people_also_discuss, user_karma_history and user_reputation_history all have zero rows. GET /api/v1/feed/trending reads trending_content, so it returns an empty list; the frontend API client defines a call for it but no page uses it.
One staleness interaction is worth stating plainly. The Reddit component of the served bullish/bearish index looks back three days from the current time. With ingestion running one to two days behind, that window in practice holds one or two days of data. At the query time above, the three-day window (from 2026-08-13) contained 2,567 rows across two days, not three.
What KarmaScan does not do
- No price data, and no sentiment-versus-price claims.
project_token_mappingshas zero rows, so no tracked project is linked to a traded token. Nothing in the product correlates sentiment with price, and no such claim is made anywhere. The only price the system fetches is BTC spot from CoinGecko, used solely to resolve the daily up-or-down prediction game. A historical BTC lookup is defined in the same module but has no caller. - No sources other than Reddit. No X/Twitter, Discord, Telegram or Farcaster ingestion exists in the codebase; those names appear only as handle fields on project listing requests. The only non-Reddit external call in the analytics path is the Alternative.me Crypto Fear and Greed index. Other external hosts do appear elsewhere in the codebase (CoinGecko, Moralis, blockchain explorers and RPCs, Resend and SendGrid) serving wallet verification, badge checks and notifications; none of them feed the corpus.
- No language handling. There is no language detection or filtering step anywhere in the pipeline, and the scoring rubric makes no reference to language. Non-English text is passed to the scoring model as-is, and no accuracy measurement has been run on non-English content.
- No deletion propagation. No code path deletes rows from
reddit_activity. The delete calls that exist target auth, wallet, follow, bookmark, watchlist, trending and related tables. Re-upserts only touch the rolling three-day window. If a redditor deletes a comment or their account four days later, KarmaScan keeps the text, the author name and the karma. There is no takedown pipeline yet. - No point-in-time guarantee. Scores are recomputed when the model or the rubric changes; a corpus-wide rescan checkpoint records 595,118 rows reprocessed. A sentiment value you read today can differ from the same row's value tomorrow. If you need a frozen historical series, export it yourself.
- No rewards, campaigns or token distribution.
reward_campaigns,sponsored_campaigns,reward_claims,supported_chainsandsupported_tokensare all empty. Nothing has been funded, distributed or claimed. - No request rate limiting on the public API. The only rate limiting in the codebase is outbound politeness delay in the scraper.
Known biases
The author base is mostly drive-by accounts. Excluding [deleted] and AutoModerator, there are 93,470 distinct authors. 67,505 of them (72.22%) appear in exactly one epoch. Those one-epoch authors account for only 17.79% of all rows. At the other end, 4,504 authors (4.82%) appear in six or more epochs and account for 55.47% of rows. Any statement about "the community" is mostly a statement about a few thousand regulars, and karma weighting pushes further in that direction.
Bot filtering is inconsistent, and the read-time exclusion is narrower than it looks. The current ingestion path drops exactly two bot accounts, iowxss6_bot and coinfeeds-bot. Their 3,604 stored rows (coinfeeds-bot 3,178, ioWxss6_bot 426) all sit in epochs 65 to 70, from the earlier CSV imports, before that filter applied. AutoModerator is not filtered at ingest at all: 4,383 AutoModerator rows are in the corpus, in every epoch.
At read time, the exclusion list ['[deleted]', 'AutoModerator'] appears in two places only: get_scope_totals, which supplies the leaderboard's total-karma and total-contributor figures, and get_category_kols, the per-category KOL boards. The ranked leaderboard itself (get_epoch_leaderboard) applies no author filter, and the Overall KOL board is built from that same function. So [deleted] can and does surface as a ranked entry: it is second by adjusted karma in epoch 78 (5,004 karma across 5,000 rows) and eleventh all time (31,996 across 31,905 rows). Other bot-named accounts are filtered nowhere: ccModBot (370 rows), MelangeBot (214), CommunityCurrencyBot (212), JeopardyQBot (148) and others. The accurate statement is therefore narrow: the leaderboard's headline totals and the per-category KOL boards exclude AutoModerator and deleted authors; ranked leaderboard rows, the Overall KOL board and corpus totals do not, and general bot detection does not exist.
Deleted authors only recently exist in the corpus at all. 31,905 rows (5.36% of the corpus) carry the author [deleted], the earliest dated 2026-01-15. Their distribution is uneven: zero in epochs 65 to 70, 240 in epoch 71, 33 in epoch 72, then between 4,536 and 6,705 per epoch in epochs 73 to 78, which is 12.4% to 17.3% of each of those epochs. The earlier importers dropped these rows; the archive-based backfill keeps them. Author-count and contributor-count comparisons across the February 2026 boundary are therefore not like for like.
Label distribution is neutral-heavy. Of 595,118 rows: 293,167 neutral (49.3%), 128,928 bearish, 94,585 bullish, 45,346 very bearish, 33,092 very bullish. No row is unlabelled. Roughly half the corpus expresses no directional stance, which is why the stance badge excludes neutrals rather than averaging them in.
Not every row is LLM-scored. 585,976 rows meet the scoring filter (text present, at least five characters). The remaining 9,142 rows (1.54%) are too short or empty to score and keep the local engine's value.
Karma keeps moving after ingestion stops watching. Scores are refreshed only inside the rolling three-day window. Content that keeps accruing karma after day three is understated in KarmaScan relative to Reddit.
A schema wart worth knowing. reddit_activity.created_at is NULL for 264,608 rows (44.5%), because the archive ingestion path does not populate it. Nothing filters on that column (it is used only as a secondary sort in the follow feed), so no aggregate is wrong because of it, but intra-day ordering for archive-era rows is effectively arbitrary. Filter on created_date if you query the database directly. Note also that created_date is stored as text, not a date type, so date arithmetic against it needs a cast.
FAQ
What is a Moon epoch, and why not calendar months?
A Moon epoch is a 28-day period that KarmaScan uses as its unit of time instead of a month. The numbering runs continuously from a fixed anchor in the code: epoch 69 starts 2025-11-10, and epoch 78 runs 2026-07-20 through 2026-08-16. The 28-day length is set to match r/CryptoCurrency's Moon cycle. It is a configured constant in KarmaScan, not a value read from Reddit, so it can drift from the subreddit if the subreddit changes its cycle.
Equal-length windows make counts easier to compare than calendar months of 28 to 31 days, but read epoch totals with the coverage caveats below before comparing them:
- The site holds data for epochs 65 through 78, covering 2025-07-21 to 2026-08-14 (595,118 rows).
- Epoch 72 holds only 12 days of data (2026-02-02 to 2026-02-13). Epoch 65 holds 27 days. Epoch 78 was still open when this data was pulled and holds 26 days. Comparing those three against a full epoch compares different amounts of time.
- Epochs 66 through 70 carry labels one day earlier than the current formula produces, a legacy of an earlier import. Checked against the formula in the code, 9,370 rows (1.6% of the corpus) sit outside the epoch they are labelled with. Epochs 71 onward match the formula exactly.
What does a sentiment score of 50 mean, and why are so many posts near 50?
Every post and comment gets an integer score from 0 to 100 for how bullish or bearish the author sounds, and 50 means no lean at all. Nothing in the corpus is unscored. The scoring rubric puts anything genuinely neutral in the 48 to 52 band: plain information, logistics, and questions that take no side. Bot and template text is scored exactly 50 by rule. News links and headline shares with no commentary from the poster also land in that band, because reporting bad news is not the same as being bearish.
Of the 595,118 posts and comments captured, 248,926 (41.8%) fall in the 48 to 52 band, and 235,254 (39.5%) score exactly 50. The exactly-50 pile is not all bots: 38,394 of those rows (16.3%) come from AutoModerator, known bot accounts, or deleted authors, and 13,439 are link posts. The rest are ordinary comments the rubric read as taking no side. A large near-neutral share is what a general-discussion subreddit produces, but treat the exact-50 pile as a mix of the rule-based cases above and genuine no-lean text rather than as a single category.
Why is someone labeled Bullish when most of their posts look neutral?
The badge measures direction only among the statements where the author took a side, and ignores the rest. Across the corpus, 266,400 of 572,465 voiced statements (46.5%) take no side at all, so close to half of what people write carries no directional signal either way. The badge is the ratio of bullish to bearish among the remaining side-taking statements, damped by how rarely the author takes a side, so a person who almost never expresses a view is pulled back toward Neutral. With fewer than 10 side-taking statements, the badge stays Neutral regardless.
One thing to know when reading badges: across all voiced content, bearish statements (179,802) outnumber bullish ones (126,263), so a Bullish badge means bullish relative to a corpus that leans bearish overall. Read the badge as an answer to one question: when this person does take a position, which way do they lean?
Why does my favorite project show fewer mentions than I expect?
A project counts once per post or comment no matter how many times its name appears in that text. The mentions table is keyed on the project and item pair, so repetition inside one long post cannot run up the count. Names are matched as whole words against a fixed keyword list of 128 projects held in the code, and the list is deliberately cautious: NEAR only counts as "$near", "near protocol", or "nearprotocol", because bare "near" is an ordinary English word. Ambiguous keywords also pass through context gates, which drop genuine mentions along with the false ones.
Coverage matters too:
- The corpus is r/CryptoCurrency only except in epochs 71 and 72. r/CryptoMarkets (3,400 rows), r/defi (461) and r/WallStreetBetsCrypto (184) appear only there, and together they are 4,045 of 595,118 rows (0.7%).
- 17 days have nothing captured at all: 2026-01-04, and 2026-02-14 through 2026-03-01.
How is your bullish/bearish number different from the fear and greed number on other crypto sites?
The headline number is 60% Alternative.me's Crypto Fear & Greed index and 40% KarmaScan's own Reddit sentiment, which is the karma-weighted, stance-weighted average of the last three days of captured discussion. The 60/40 split is a configurable default, not a fixed constant. It tracks the market index but pulls away from it when Reddit discussion leans differently.
KarmaScan labels the gauge in market-stance terms instead of the Fear/Greed wording other sites use. The bands are: below 15 Extreme Bearish, 15 to under 35 Bearish, 35 to under 55 Neutral, 55 to under 75 Bullish, 75 and up Extreme Bullish. The same number can sit in a different band on a site that uses different cutoffs.
The market half refreshes at most once an hour and the Reddit half comes from stored data that can run a day or two behind, so treat it as a daily reading rather than a live ticker. If the market index cannot be fetched and nothing is cached, the API falls back to the Reddit-only number and flags the response as reddit_only. Be aware that the gauge on the page does not print a fallback notice: it simply stops showing the "Market x, Reddit y" breakdown line that appears when both halves are present.
How do you stop a common word like "compound" or "ledger" from being counted as a project?
Two mechanisms do the work. Matching is on whole words and never on substrings, so "pol" cannot fire on "police" and "sol" cannot fire on "sold". Ambiguous keywords then carry a context gate: "ledger" counts only when the text also mentions a wallet, hardware, nano, cold storage, a seed phrase, or similar, and "compound" counts only alongside DeFi vocabulary such as lending, borrow, aave, or protocol. Some keywords also carry a blocklist that discards the hit outright, so "compound interest" and "compounding" never register as the lending protocol. Eleven projects carry gates of this kind, and eleven carry blocklists.
The effect is measurable and intentionally conservative. The word "ledger" appears in 2,007 captured items and 562 of those are counted as the company. The word "compound" appears in 152 items and 34 of those are counted as the protocol (Compound is counted on 35 items in total, the extra one matching on the "$comp" ticker instead).
Does KarmaScan use my Reddit data? What if I delete a post?
KarmaScan reads public r/CryptoCurrency content and stores, for each post or comment, the author name, the permalink, the title or comment text, the raw score and the flair-adjusted score, the flair, the subreddit, the post type, and the timestamp, plus the sentiment and project attribution derived from it. There is no Reddit OAuth connection, so nothing private is reachable. There is an optional account-linking flow, but it works by reading a code you post publicly, not by logging into your Reddit account.
Deleting a post on Reddit does not remove the copy already stored here. Each ingest run re-reads roughly the last three days and updates only the score, sentiment, and project attribution of an existing row. It never rewrites the stored author or text, and it has no delete path. Older rows stay as captured.
There is no automated removal path in the API. No endpoint deletes captured Reddit content. If you want your content removed, use the contact form and it is handled manually. The form has no dedicated removal topic, so pick "General enquiry".
Can I get my project added?
Yes, and it is a paid listing at 500 USDC. You submit the listing form with your project name, category, 1 to 10 keywords, and a Telegram handle, and you get a request ID back. After the request, the founder opens a private Telegram group with you to agree keywords and scope. Note the order: on the current flow that conversation happens after payment, not before it, so agree anything you need in writing before you send funds.
Payment buys tracking: your keywords are added and your project appears in the boards from the go-live date forward. There is no historical backfill. It does not buy a better sentiment score, because scoring runs against a fixed rubric that has no idea who paid.
The listing page currently describes an onchain escrow with a 30-day reclaim. Treat that as not yet in force. The escrow contract address is not configured in this deployment, and the API returns a null contract address, so payment terms should be confirmed directly with the founder.
Can I get the data?
Use the contact form and choose the "Data or API access" topic. There is no published price list, no self-serve plan, and no signup tier, so access is arranged case by case in that conversation. The public data endpoints that serve the site are read-only reads and cover projects, users, leaderboards, epochs, categories, and sentiment history. There is no export or bulk-download endpoint in the product today, so anything beyond those endpoints is a manual pull.
Is the leaderboard karma the same as my Reddit karma?
No. Reddit karma is your sitewide total across every subreddit and your entire account history. The KarmaScan number is the sum of the flair-adjusted scores on the posts and comments captured for you, in the tracked subreddits, inside a single 28-day epoch.
Three things change it:
- Scores are captured at ingest and refreshed for roughly three days after posting, so votes that arrive later are not picked up.
- Meme-flaired posts are discounted rather than counted at face value: at 0.25% of the score from epoch 71 onward, and at about 25% in epochs 65 to 70, which came from an earlier import.
- A small number of per-user, per-epoch adjustments are held in configuration and applied after aggregation. They are manually set founder decisions, not measured values, and the API response carries no marker showing one was applied. At the time of writing they affect one username.
Glossary
Mindshare. A project's share of all counted mentions in a period, expressed as a percentage. It is computed and stored, but the API and the project pages do not currently return or display it. The project pages show mention counts and sentiment instead.
Stance. How far a piece of content leans away from neutral 50. It is used two ways: as a weight in averages, where opinionated content counts more and near-neutral filler is damped without being silenced entirely (the weight has a floor of 0.1, so no row is dropped), and as the Bullish or Bearish badge on a user profile.
Voiced content. Comments and text posts, meaning things the author wrote themselves. Link and image shares are excluded from stance work, because sharing a headline is not the same as taking a position. 572,465 of the 595,118 captured items are voiced; the other 22,653 are link, image, and other non-text posts.
Epoch karma. The sum of the Reddit scores on an author's captured posts and comments inside one 28-day epoch, after the meme-flair discount and after any configured per-user adjustment. It is not sitewide Reddit karma.
Composite index. The bullish/bearish headline on the dashboard: 60% Alternative.me's Crypto Fear & Greed index plus 40% KarmaScan's karma-weighted, stance-weighted Reddit sentiment from the last three days.
KOL board. A per-category leaderboard of Reddit authors, ranked by the karma they earned on posts and comments that mention projects in that category. Deleted authors and AutoModerator are excluded. A small set of manually configured founder overrides can replace an author's computed category karma and promote them into the visible top of a board, so a board position is not always a pure aggregation. There are 12 boards: Overall plus Blockchains, DeFi, Trading, Prediction Markets, Wallets, Meme, Politics, Exchanges, NFTs, Gaming, and AI / Research.
Context gate. A rule attached to one keyword rather than a whole project, requiring other words to appear before the mention counts, or discarding the hit when certain words appear. "Ledger" needs wallet or hardware vocabulary nearby; "compound" is thrown out when the text says "compound interest".
Still have a question?
Questions about the methodology, access to the data for your team, or a number that looks wrong all reach a person directly.
Contact us