1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
//! Deployer analysis: the dev wallet (from Jupiter's index), its age, how much
//! of the supply it still holds, and how many OTHER tokens it has launched —
//! the serial-rugger signal.
use crate::chain::jupiter::JupToken;
use crate::chain::SolClient;
use crate::types::{Coverage, Flag, Severity};
use serde_json::{json, Value};
pub async fn analyze(
client: &SolClient,
jup: Option<&JupToken>,
dev_fallback: Option<&str>,
cov: &mut Coverage,
) -> (Value, Vec<Flag>) {
let mut flags = vec![];
// Prefer Jupiter's indexed dev; fall back to the creation-tx fee payer
// (resolved by the bundle pass) when Jupiter hasn't indexed the token, so
// the wallet-age / fresh-deployer read isn't silently skipped. The
// serial-deployer + dev-holdings signals below stay Jupiter-only (they need
// its cross-mint index) and honestly go quiet when Jupiter has no data.
let dev = jup
.and_then(|j| j.dev.clone())
.or_else(|| dev_fallback.map(|s| s.to_string()));
let dev_mints = jup.and_then(|j| j.audit.dev_mints);
let dev_balance_pct = jup.and_then(|j| j.audit.dev_balance_percentage);
let mut wallet_age_days: Option<i64> = None;
let mut wallet_tx_floor: Option<usize> = None;
if let Some(d) = &dev {
// A failed history fetch must NOT read as "no history" — that would let
// a brand-new deployer wallet publish identically to one we checked and
// found established. Mark coverage so the verdict cap engages.
match client.signatures(d, 1000, None).await {
Err(e) => cov.degrade(format!("deployer wallet history unreadable ({e})")),
Ok(hist) => {
wallet_tx_floor = Some(hist.len());
if hist.len() < 1000 {
if let Some(oldest) = hist.last().and_then(|s| s.block_time) {
let now = time::OffsetDateTime::now_utc().unix_timestamp();
wallet_age_days = Some((now - oldest) / 86_400);
}
}
if hist.len() <= 5 {
flags.push(Flag::new(
"fresh_deployer",
Severity::Warning,
"Fresh deployer wallet",
format!(
"The creator wallet {} has only {} transactions — no track record at all.",
short(d),
hist.len()
),
5,
));
}
}
}
}
if let Some(n) = dev_mints {
if n >= 1000 {
// A count this high is a SHARED launchpad/factory authority (e.g.
// a pump.fun-class creation wallet), not a person — Jupiter's
// "dev" attribution stops meaning "the creator" here. Flagging it
// as a serial rugger was a false positive on every graduated
// launchpad coin (observed live 2026-07-15: "130802 tokens").
flags.push(Flag::new(
"factory_authority",
Severity::Info,
"Created through a shared launchpad authority",
format!("The creation wallet has minted {n} tokens — it's a launchpad/factory authority, not an individual creator, so deployer history isn't attributable to a person. Judge the token on its own checks."),
0,
));
} else if n >= 50 {
// A COUNT, not a fraud finding. Solwatch has no rug history for
// these mints — it never checks whether a single prior token was
// rugged (no LP-pull history, no authority abuse, no price death),
// and deploy bots / market-making infra / NFT issuers all cross this
// line legitimately. The old copy ("classic churn-and-rug factory
// behavior") asserted fraud about a NAMED wallet on the strength of
// one third-party integer. Report the count; let the reader judge.
// The weight stays as a base-rate prior, which is what it actually is.
flags.push(Flag::new(
"serial_deployer",
Severity::Warning,
"High-volume token creator",
format!(
"This creation wallet has minted {n} tokens. High-volume deployers include both rug factories and legitimate bots/launch services — Solwatch has NOT assessed how those tokens performed, so treat this as context, not a finding."
),
12,
));
} else if n >= 10 {
flags.push(Flag::new(
"serial_deployer",
Severity::Warning,
"Creator has launched many tokens",
format!("This creator has launched {n} tokens before this one."),
6,
));
}
}
// Jupiter's indexed dev balance. It is a THIRD-PARTY number and can be
// stale (a dev who has since sold still reads as holding), which is why the
// copy attributes it — otherwise the report can contradict its own on-chain
// holder table on the same page and undermine every other figure in it.
if let Some(p) = dev_balance_pct {
let p2 = (p * 100.0).round() / 100.0;
if p >= 20.0 {
flags.push(Flag::new(
"dev_holdings",
Severity::Danger,
"Dev holds a huge bag",
format!("Jupiter's index shows the creator holding {p2}% of the supply — one click from a rug. Cross-check the holder table below; index data can lag a sale."),
15,
));
} else if p >= 10.0 {
flags.push(Flag::new(
"dev_holdings",
Severity::Warning,
"Dev holds a large bag",
format!("Jupiter's index shows the creator holding {p2}% of the supply (third-party data — cross-check the holder table)."),
8,
));
}
} else if jup.is_none() {
// Both cross-mint signals (serial deployer + dev holdings) need
// Jupiter's index. With it down they vanish silently, so a serial
// rugger with a 45% bag loses ~27 weight points. Mark the gap.
cov.degrade("deployer history + dev holdings unavailable (Jupiter index)");
}
let section = json!({
"dev": dev,
"walletAgeDays": wallet_age_days,
"walletTxCount": wallet_tx_floor.map(|n| if n >= 1000 { json!("1000+") } else { json!(n) }),
"tokensLaunched": dev_mints,
"devBalancePct": dev_balance_pct,
});
(section, flags)
}
fn short(a: &str) -> String {
if a.len() < 12 {
a.to_string()
} else {
format!("{}…{}", &a[..4], &a[a.len() - 4..])
}
}