semantic-types PyPI Solana Keypair Monkey Patch
Socket reported that semantic-types became malicious at version 0.1.5 and 0.1.6, with five Solana-themed PyPI packages pulling it transitively. The payload monkey-patched solders.keypair.Keypair constructors, encrypted Solana private keys with an RSA-2048 public key, and exfiltrated ciphertext through Solana Devnet SPL memo transactions.
On this page 0% read
Executive Summary
Socket reported a PyPI campaign by the alias cappership in which semantic-types carried the malicious payload and five Solana-themed packages pulled it transitively: solana-keypair, solana-publickey, solana-mev-agent-py, solana-trading-bot, and soltrade [Source 1].
The malicious semantic-types update landed at 0.1.5 on January 26, 2025; 0.1.6 repackaged the same payload on January 28, 2025 [Source 1]. The payload monkey-patched solders.keypair.Keypair.from_seed, from_bytes, and from_base58_string, encrypted captured private key bytes with a hardcoded RSA-2048 public key, and sent the ciphertext as an SPL memo transaction through Solana Devnet RPC at api.devnet.solana.com [Source 1].
There is no conventional attacker C2 domain to block for the key theft path. The hard indicators are package names/versions, PyPI publisher metadata from the Socket report, the Solana Devnet RPC endpoint, the SPL Memo program ID, the actor public key D782zqWjgSvy4hQoqzY1ySrGrotnXm1suJeXFur8sAko, the RSA public-key fingerprint 5a4d8480c9d1e82ba102f200258882fb9e694e8fc0343b6982c5540beccdca62, and code paths that generate Solana keypairs while the malicious package is present [Source 1].
Key Facts
event_type: "malicious PyPI package with transitive dependency delivery"
ecosystem: "PyPI"
publisher:
alias: "cappership"
email: "cappership@proton.me"
malicious_payload_package:
name: "semantic-types"
malicious_versions:
- "0.1.5"
- "0.1.6"
transitive_carrier_packages:
- "solana-keypair"
- "solana-publickey"
- "solana-mev-agent-py"
- "solana-trading-bot"
- "soltrade"
execution_trigger: "Python import that registers monkey patches, followed by solders Keypair constructor use"
patched_methods:
- "solders.keypair.Keypair.from_seed"
- "solders.keypair.Keypair.from_bytes"
- "solders.keypair.Keypair.from_base58_string"
collection_window_utc:
start: "2025-01-26T00:00:00Z"
end: "2025-05-29T23:59:59Z"
network_iocs:
- "api.devnet.solana.com"
solana_iocs:
memo_program_id: "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr"
actor_public_key: "D782zqWjgSvy4hQoqzY1ySrGrotnXm1suJeXFur8sAko"
crypto_iocs:
rsa_public_key_fingerprint_sha256: "5a4d8480c9d1e82ba102f200258882fb9e694e8fc0343b6982c5540beccdca62"
credentials_at_risk:
- "Solana private keys created or imported through patched solders Keypair constructors"
Source Confidence & Evidence Mapping
- confirmed: Socket identified
semantic-typesas the core malicious package andsolana-keypair,solana-publickey,solana-mev-agent-py,solana-trading-bot, andsoltradeas dependent carrier packages [Source 1]. - confirmed: Socket states
semantic-types 0.1.5introduced the malicious payload on January 26, 2025 and0.1.6repackaged it on January 28, 2025 [Source 1]. - confirmed: Socket identified the monkey-patched methods
Keypair.from_seed,Keypair.from_bytes, andKeypair.from_base58_string, the Solana Devnet RPC endpoint, the threat actor public key, the PyPI alias/email, and RSA public-key fingerprint [Source 1]. - unclear: Public sources do not name victim wallets or prove which Devnet memo transactions correspond to real production keypairs.
- not_observed: No evidence indicates compromise of the legitimate
solderspackage itself.
Impact Determination
| Classification | Criteria | Evidence to collect | Handling decision |
|---|---|---|---|
| Confirmed compromise | A malicious package was installed and a patched solders.keypair.Keypair constructor generated or imported a real Solana keypair. | Installed package/version evidence, code path invoking Keypair.from_seed, Keypair.from_bytes, or Keypair.from_base58_string, Solana Devnet RPC sendTransaction, memo program use, actor public key D782zqWjgSvy4hQoqzY1ySrGrotnXm1suJeXFur8sAko. | Treat keypairs created or imported through the process as exposed; move funds and authority to keys generated in a clean environment. |
| Presumed exposed | semantic-types==0.1.5 or 0.1.6, or any carrier package, was installed in an environment that runs Solana key generation/import code, but runtime telemetry is missing. | pip freeze, lockfile, package cache, source grep for patched methods, notebook history, CI output. | Inventory all keypairs generated or imported by that environment after January 26, 2025 and replace them from clean tooling. |
| Potentially exposed | A carrier package appears in manifests or lockfiles, but installation or malicious dependency resolution is incomplete. | Dependency graph, package proxy records, lockfile history, virtualenv/container inventory. | Collect package-cache and environment evidence until the asset is classified. |
| Not exposed | No malicious package, carrier package, affected version, Solana keypair constructor, Devnet RPC, memo program, or actor key appears in source, environments, caches, or telemetry. | Negative repository search, package inventory, dependency lock, virtualenv/container search, and network telemetry search. | Keep negative evidence with the case record and close this event for the asset. |
| Unknown | Package inventory, dependency resolution history, endpoint data, source history, or Solana RPC telemetry is unavailable. | Named telemetry gap with system, owner, and retention status. | Keep Solana key material in scope until the missing evidence is recovered or key replacement is accepted as the closure path. |
Minimum Evidence To Collect
package_evidence:
- "semantic-types==0.1.5"
- "semantic-types==0.1.6"
- "solana-keypair"
- "solana-publickey"
- "solana-mev-agent-py"
- "solana-trading-bot"
- "soltrade"
code_evidence:
- "solders.keypair.Keypair.from_seed"
- "solders.keypair.Keypair.from_bytes"
- "solders.keypair.Keypair.from_base58_string"
network_evidence:
- "api.devnet.solana.com"
- "sendTransaction"
- "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr"
- "D782zqWjgSvy4hQoqzY1ySrGrotnXm1suJeXFur8sAko"
crypto_evidence:
- "5a4d8480c9d1e82ba102f200258882fb9e694e8fc0343b6982c5540beccdca62"
Timeline
- 2024-12-22: Socket reports benign
semantic-types 0.1.2andsolana-trading-bot 0.1.0were published [Source 1]. - 2024-12-23: Socket reports updates for
semantic-types 0.1.4,solana-trading-bot 0.1.1, andsoltrade 0.1.1with dependencies still benign [Source 1]. - 2025-01-26: Socket reports
semantic-types 0.1.5introduced the malicious monkey-patching payload;solana-mev-agent-py 0.1.0andsolana-keypair 0.1.0were also published [Source 1]. - 2025-01-28: Socket reports
semantic-types 0.1.6repackaged the same malicious payload [Source 1]. - 2025-02-04: Socket reports
solana-keypair 0.2.1andsolana-publickey 0.2.1were released and importedsemantic-types[Source 1]. - 2025-05-29: Socket published the public analysis with IOCs and the package cluster [Source 1].
What Happened
The actor used dependency relationships rather than a one-package-only lure. semantic-types contained the payload, while five Solana-themed packages depended on it and served as delivery paths [Source 1].
Once imported, the payload modified methods on solders.keypair.Keypair at runtime. Calls to from_seed, from_bytes, and from_base58_string still returned a usable keypair, but the wrapper also sent key material to the attacker’s collection path through a background thread [Source 1].
The exfiltration path used normal Solana Devnet RPC. Captured private key bytes were encrypted with the actor’s RSA public key, base64 encoded, embedded in an SPL Memo transaction, and broadcast to Devnet. The attacker could later read memo transactions associated with the actor public key and decrypt ciphertext offline [Source 1].
Technical Analysis
Package Manipulation
payload_package: "semantic-types"
malicious_versions:
- "0.1.5"
- "0.1.6"
carrier_packages:
- "solana-keypair"
- "solana-publickey"
- "solana-mev-agent-py"
- "solana-trading-bot"
- "soltrade"
dependency_trigger: "pip resolves semantic-types from carrier package dependencies"
Runtime Hook
The hook targets the solders keypair class object already used by Solana Python developers. Monkey patching means source code that imports solders can look normal while runtime method dispatch has changed inside the interpreter [Source 1].
Exfiltration
transport:
protocol: "Solana JSON-RPC over HTTPS"
endpoint: "https://api.devnet.solana.com"
rpc_method: "sendTransaction"
program: "SPL Memo"
memo_program_id: "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr"
actor_public_key: "D782zqWjgSvy4hQoqzY1ySrGrotnXm1suJeXFur8sAko"
payload_encoding:
key_material: "64-byte private key material from Keypair bytes"
encryption: "RSA-2048 public key"
encoding: "Base64 ciphertext in memo data"
Affected Assets and Blast Radius
affected_assets:
ecosystems:
- "PyPI"
packages:
- "semantic-types==0.1.5"
- "semantic-types==0.1.6"
- "solana-keypair"
- "solana-publickey"
- "solana-mev-agent-py"
- "solana-trading-bot"
- "soltrade"
environments:
- "developer virtualenvs"
- "CI jobs"
- "Jupyter notebooks"
- "container images"
- "Solana bots and trading tools"
secret_material:
- "Solana private keys generated by patched methods"
- "Solana private keys imported through patched methods"
not_currently_known_to_affect:
- "the solders package itself"
- "Solana keypairs generated in clean environments without the malicious packages"
Indicators of Compromise
package_versions:
- "semantic-types==0.1.5"
- "semantic-types==0.1.6"
- "solana-keypair"
- "solana-publickey"
- "solana-mev-agent-py"
- "solana-trading-bot"
- "soltrade"
domains:
- "api.devnet.solana.com"
urls:
- "https://api.devnet.solana.com"
solana:
memo_program_id: "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr"
actor_public_key: "D782zqWjgSvy4hQoqzY1ySrGrotnXm1suJeXFur8sAko"
crypto:
rsa_public_key_fingerprint_sha256: "5a4d8480c9d1e82ba102f200258882fb9e694e8fc0343b6982c5540beccdca62"
python_symbols:
- "solders.keypair.Keypair.from_seed"
- "solders.keypair.Keypair.from_bytes"
- "solders.keypair.Keypair.from_base58_string"
Detection and Hunting
Script: Python dependency, Solana key-generation, and Devnet RPC sweep
#!/usr/bin/env python3
import json
import os
import subprocess
import sys
import urllib.request
from pathlib import Path
ROOT = Path(sys.argv[1] if len(sys.argv) > 1 else ".").resolve()
LOG_ROOT = Path(os.environ.get("LOG_ROOT", "")).resolve() if os.environ.get("LOG_ROOT") else None
OUT = Path(os.environ.get("OUT", "hp-semantic-types-solana-scope")).resolve()
COLLECTION_START = "2025-01-26T00:00:00Z"
COLLECTION_END = "2025-05-29T23:59:59Z"
PACKAGES = [
"semantic-types",
"solana-keypair",
"solana-publickey",
"solana-mev-agent-py",
"solana-trading-bot",
"soltrade",
]
SELECTORS = [
"semantic-types==0.1.5",
"semantic-types==0.1.6",
"semantic-types",
"solana-keypair",
"solana-publickey",
"solana-mev-agent-py",
"solana-trading-bot",
"soltrade",
"solders.keypair",
"Keypair.from_seed",
"Keypair.from_bytes",
"Keypair.from_base58_string",
"api.devnet.solana.com",
"sendTransaction",
"MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr",
"D782zqWjgSvy4hQoqzY1ySrGrotnXm1suJeXFur8sAko",
"5a4d8480c9d1e82ba102f200258882fb9e694e8fc0343b6982c5540beccdca62",
]
DEPENDENCY_FILES = {
"requirements.txt",
"requirements.lock",
"pyproject.toml",
"poetry.lock",
"uv.lock",
"Pipfile.lock",
"environment.yml",
"conda-lock.yml",
"sbom.json",
"cyclonedx.json",
}
# Positive signal: a malicious package/carrier package selector appears with solders Keypair constructor code or Solana Devnet RPC telemetry.
# Escalation: malicious package evidence plus Keypair constructor use after COLLECTION_START requires replacement of Solana keys generated or imported in that process.
OUT.mkdir(parents=True, exist_ok=True)
(OUT / "matches").mkdir(exist_ok=True)
(OUT / "python-env").mkdir(exist_ok=True)
(OUT / "solana-devnet").mkdir(exist_ok=True)
selectors_file = OUT / "semantic-types-selectors.txt"
selectors_file.write_text("\n".join(SELECTORS) + "\n")
def scan_tree(base: Path, label: str) -> list[dict]:
findings = []
if not base.exists():
return findings
skip = {".git", "node_modules", ".venv", "venv", "__pycache__", "dist", "build"}
for root, dirs, files in os.walk(base):
dirs[:] = [d for d in dirs if d not in skip]
for name in files:
p = Path(root) / name
if p.stat().st_size > 50_000_000:
continue
try:
data = p.read_text(errors="ignore")
except Exception:
continue
hits = [s for s in SELECTORS if s in data]
if hits:
findings.append({"scope": label, "path": str(p), "hits": sorted(set(hits))})
return findings
findings = scan_tree(ROOT, "root")
if LOG_ROOT:
findings.extend(scan_tree(LOG_ROOT, "log_root"))
(OUT / "matches" / "selector-matches.jsonl").write_text(
"".join(json.dumps(x, sort_keys=True) + "\n" for x in findings)
)
(OUT / "matches" / "dependency-file-hits.jsonl").write_text(
"".join(json.dumps(x, sort_keys=True) + "\n" for x in findings if Path(x["path"]).name in DEPENDENCY_FILES)
)
for item in findings:
print(f"[MATCH] {item['scope']} {item['path']} :: {', '.join(item['hits'])}")
for pkg in PACKAGES:
res = subprocess.run(["python3", "-m", "pip", "show", pkg], text=True, capture_output=True)
(OUT / "python-env" / f"pip-show-{pkg}.txt").write_text(res.stdout + res.stderr)
rpc_payload = {
"jsonrpc": "2.0",
"id": 1,
"method": "getSignaturesForAddress",
"params": [
"D782zqWjgSvy4hQoqzY1ySrGrotnXm1suJeXFur8sAko",
{"limit": 1000},
],
}
req = urllib.request.Request(
"https://api.devnet.solana.com",
data=json.dumps(rpc_payload).encode(),
headers={"Content-Type": "application/json"},
)
try:
with urllib.request.urlopen(req, timeout=20) as resp:
(OUT / "solana-devnet" / "actor-key-signatures.json").write_bytes(resp.read())
except Exception as exc:
(OUT / "solana-devnet" / "actor-key-signatures.error.txt").write_text(str(exc))
if findings:
print(f"[!] {len(findings)} selector-bearing files written under {OUT}")
else:
print(f"[+] No semantic-types/Solana incident selectors found under {ROOT}")
Downstream Abuse Audits
Script: Solana key-use and wallet replacement scoping collector
#!/usr/bin/env python3
import json
import os
import subprocess
import sys
from pathlib import Path
ROOT = Path(sys.argv[1] if len(sys.argv) > 1 else ".").resolve()
OUT = Path(os.environ.get("OUT", "hp-semantic-types-solana-downstream")).resolve()
SINCE = "2025-01-26T00:00:00Z"
UNTIL = "2025-05-29T23:59:59Z"
SELECTORS = [
"semantic-types==0.1.5",
"semantic-types==0.1.6",
"solana-keypair",
"solana-publickey",
"solana-mev-agent-py",
"solana-trading-bot",
"soltrade",
"Keypair.from_seed",
"Keypair.from_bytes",
"Keypair.from_base58_string",
"solders.keypair",
"api.devnet.solana.com",
"MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr",
"D782zqWjgSvy4hQoqzY1ySrGrotnXm1suJeXFur8sAko",
]
# Positive signal: repository, notebook, CI output, or package inventory contains malicious package evidence and Solana Keypair constructor use after SINCE.
# Remediation trigger: any real wallet key generated or imported through a matched runtime must be replaced from a clean environment, and authority/funds must move to the replacement key.
OUT.mkdir(parents=True, exist_ok=True)
(OUT / "git").mkdir(exist_ok=True)
(OUT / "matches").mkdir(exist_ok=True)
selectors_file = OUT / "selectors.txt"
selectors_file.write_text("\n".join(SELECTORS) + "\n")
def run(cmd: list[str], outfile: Path, cwd: Path | None = None) -> None:
res = subprocess.run(cmd, cwd=cwd, text=True, capture_output=True)
outfile.write_text(res.stdout + res.stderr)
run(["rg", "-n", "--hidden", "--fixed-strings", "-f", str(selectors_file), str(ROOT)], OUT / "matches" / "selector-search.txt")
run(["rg", "-n", "Keypair\\.(from_seed|from_bytes|from_base58_string)|solders\\.keypair|solana_keypair|solana-publickey", str(ROOT)], OUT / "matches" / "keypair-code-paths.txt")
run(["python3", "-m", "pip", "freeze"], OUT / "matches" / "pip-freeze.txt")
if (ROOT / ".git").exists():
run(["git", "log", "--since", SINCE, "--until", UNTIL, "--all", "--stat", "--", "requirements.txt", "pyproject.toml", "poetry.lock", "uv.lock", "Pipfile.lock"], OUT / "git" / "dependency-history.txt", ROOT)
run(["git", "grep", "-n", "-e", "Keypair.from_seed", "-e", "Keypair.from_bytes", "-e", "Keypair.from_base58_string", "--", "*.py", "*.ipynb"], OUT / "git" / "keypair-git-grep.txt", ROOT)
manifest = {
"case": "semantic-types-solana-keypair-monkey-patch",
"since": SINCE,
"until": UNTIL,
"remediation_trigger": "Replace Solana keys generated or imported by matched code paths while semantic-types 0.1.5 or 0.1.6 was installed.",
"evidence_files": [
str(OUT / "matches" / "selector-search.txt"),
str(OUT / "matches" / "keypair-code-paths.txt"),
str(OUT / "matches" / "pip-freeze.txt"),
str(OUT / "git" / "dependency-history.txt"),
str(OUT / "git" / "keypair-git-grep.txt"),
],
}
(OUT / "case-manifest.json").write_text(json.dumps(manifest, indent=2, sort_keys=True))
subprocess.run(["rg", "-n", "--hidden", "--fixed-strings", "-f", str(selectors_file), str(OUT)])
print(f"[+] Downstream artifacts written under {OUT}")
Sources
- Socket: Monkey-Patched PyPI Packages Use Transitive Dependencies to Steal Solana Private Keys - Role: PRIMARY_RESEARCH - Impact: Package cluster, malicious versions, payload behavior, patched methods, Solana Devnet exfiltration, actor key, RSA fingerprint, and PyPI publisher identity.