Catalyst · Adoption oracle
The adoption oracle
Catalyst has a problem that most token launch protocols ignore: the price of a token on a DEX is not a reliable signal of whether a project is actually being adopted. A bad actor with two wallets can drive the price up by trading between themselves, generating fake volume and fake appreciation, and walking away with investor money after the settlement evaluates as success. This is called wash trading, and it is endemic to token launches.
The adoption oracle is the answer. It is the component of Catalyst that separates manufactured traction from real adoption, and publishes that judgment on-chain so the performance evaluator can use it at settlement. This page explains what the oracle is, how it works, what it measures, how it decides, and why it exists as a separate piece rather than being baked directly into the evaluator.
What the oracle is (and isn't)
“The oracle” is actually two things working together:
- On-chainThe
CatalystAdoptionScorersmart contract. It holds the current verdict (PENDING / APPROVE / DENY), confidence level, and reasoning CID for every startup. It only accepts updates from a single authorized address (the oracle signer). The performance evaluator reads from it at settlement. - Off-chainThe
scorer-oracledaemon (a Node.js service in thepackages/scorer-oracle/workspace). It runs continuously, reads token activity and pool state directly from the chain, applies a set of thresholds to detect wash trading and adoption patterns, and signs transactions to update the on-chain scorer.
The oracle is nota price oracle. It does not feed prices into anything. Prices are read directly from Uniswap v4's StateView by the performance evaluator. The oracle is exclusively about the quality of the activity behind the price.
Why Catalyst needs an oracle and Lockstep doesn't
This is a fair question because Lockstep ↗ doesn't have an oracle at all. The reason comes down to what each protocol is trying to measure at settlement.
Lockstep measures trading success with a deterministic formula: at the end of the commitment window, does the final portfolio balance exceed the initial capital plus the minimum promised return? This is unambiguous and requires no judgment — two numbers, one comparison, done. The evaluator can do it with zero oracle support.
Catalyst measures startup success with a different question: did the token appreciate and is that appreciation real? The first half is deterministic (read the price from Pool 2). The second half is not. Wash trading can produce apparent price gains without any real adoption, and a naive evaluator that only looks at price would reward manipulation. The oracle exists to answer the second half — the part that requires reading holder behavior, trading patterns, and retention.
What the oracle measures
The scorer-oracle daemon combines two collectors:
Manual collector (seed data)
For startups that are too new to have meaningful on-chain activity, or for pilot deployments, the manual collector reads metrics from a YAML file maintained by the admin. This is a transitional mechanism: it lets Catalyst function during bootstrap before there is enough on-chain data to reason from. In production, manual entries are phased out as the on-chain collector takes over.
On-chain collector (the real one)
The on-chain collector reads ERC-20 Transfer events from the startup's token and swap events from Pool 2, then derives five key metrics:
- Holder count. The number of unique addresses holding a non-zero token balance. Low counts suggest concentrated ownership and a higher risk of coordinated wash activity. Threshold:
minHolders = 20. - Unique swappers. The number of distinct addresses that have executed a swap in Pool 2. Distinct swappers is a weaker signal than holders but catches cases where holders are diverse but trading is driven by one or two accounts. Threshold:
minSwappers = 10. - Trading volume. Total USDx volume through Pool 2 over the measurement window. Combined with swapper diversity, gives a sense of whether the activity is real. Threshold:
minVolume = 1000 USDC. - Wash trading score. A heuristic score from 0 to 100 that detects circular trading patterns: the same address buying and selling in close succession, pairs of addresses trading back and forth, suspicious burst timing. Higher means more washy. Threshold:
maxWashScore = 60. - 7-day holder retention. Of the addresses that held the token 7 days ago, what fraction still hold it now? Real adoption has high retention (people are HODLing). Pump-and-dumps have low retention (early holders dump as soon as the price moves). Threshold:
minRetention = 0.4 (40%).
How the verdict is decided
The oracle counts how many of the five metrics fail their threshold, and applies a decision table:
| Condition | Verdict | Confidence |
|---|---|---|
washScore > 90 | DENY | 95% |
| 3 or more thresholds failed | DENY | 70% |
| 1-2 thresholds failed | PENDING | 50% |
| 0 thresholds failed | APPROVE | 80% |
Note that a very high wash score is a hard DENY on its own, regardless of the other metrics. Also note that confidence never reaches 100% — the oracle is explicit about the fact that all heuristics have error bars, and downstream logic (including potential human dispute mechanisms) should treat the verdict as a strong signal, not as absolute truth.
Reasoning CIDs
Every verdict comes with a reasoningCID field — an IPFS content ID pointing to a JSON document that contains all the raw metrics, the computed scores, and the specific reasoning that led to the verdict. This is critical for two reasons:
- • Auditability. Anyone can fetch the CID and see exactly what data the oracle looked at and how it decided. No black box.
- • Disputability. If a founder disagrees with a DENY verdict, they can file a dispute that references the specific reasoning CID. The dispute mechanism can then audit the claim.
The full flow, end to end
1. scorer-oracle daemon wakes up on interval (e.g. every 5 minutes)
2. For each active startup, it queries:
— ERC-20 Transfer events from the token
— Swap events from Pool 2
— Pool state via StateView
3. Computes the 5 metrics + aggregate wash score
4. Applies the decision table to produce verdict + confidence
5. Writes reasoning JSON to IPFS, gets a CID
6. Signs a tx with ORACLE_PRIVATE_KEY calling postScore(id, verdict, confidence, cid)
7. Transaction confirms → on-chain scorer updated
8. Backend indexer picks up the event, records it in the scores table
9. Frontend admin dashboard shows the new verdict for the startup
10. At commitment deadline, PerformanceEvaluator reads the latest verdict
Security posture
⚠️What happens if the oracle is compromised
The oracle signing key lives on a server running 24/7. If an attacker gains control of that key, they can post arbitrary verdicts. What they cannot do:
- • Drain any funds (the oracle address has no claim on investor capital)
- • Slash anyone's collateral directly
- • Modify startup parameters
- • Take over the protocol
The worst an oracle attacker can do is mark a legitimate startup as DENY (griefing) or mark a fraudulent startup as APPROVE. Both are serious but recoverable: the admin can rotate the oracle address via setOracle(newAddress), and disputes can be filed against individual verdicts.
This is why the oracle role is strictly scoped to a single function on a single contract, and why best practice is to run the oracle signing key on a dedicated wallet separate from the protocol admin. In the current deploy the two roles share a key for simplicity, but they are independent and rotatable.
Configuration
The oracle's thresholds are defined in the daemon's config and can be tuned per deployment. The defaults listed on this page are calibrated for Base Sepolia testnet. Production mainnet defaults will be more conservative (higher minHolders, lower maxWashScore, longer measurement windows).
The daemon is hosted in packages/scorer-oracle/ within the Catalyst monorepo. It runs under PM2 alongside the backend indexer.
Where to go next
- How it works — see how the verdict fits into the overall lifecycle
- Architecture — where the daemon sits in the overall system
- Roles — the oracle role's permissions in full