内容
# NFT 洗钱交易记忆代理
一个基于记忆的代理,检测 NFT 洗钱交易并**随着时间推移学习更早地发现它**。当一个洗钱交易钱包对被确认后,代理不仅记录该模式,还会追溯到钱包的完整历史,挖掘出在明显的循环交易之前的最早的前兆信号,并将它们存储为一个不断增长的**攻略手册**,存放在 CockroachDB 中。每个确认的案例都使得下一次检测更早更准确。
为 **CockroachDB × AWS "Build with Agentic Memory" 黑客马拉松** 而建。
> **数据与范围:** 仅限公共链上数据(Dune Analytics `nft.trades` + Etherscan)。目标:LooksRare / Ethereum / 2022,据独立研究(Niu 等人,2024)估计,约 94.5% 的交易量是洗钱交易。不使用任何专有或交易所内部数据或方法。
---
## 演示
```bash
uvicorn app.main:app --port 8100 # 然后打开 http://localhost:8100
```
### 1. 每日监控 — 分析师的晨间工作清单
定时运行扫描每日新交易,检测新的交易环,并根据与**内存中已确认的案例**的匹配程度进行排名。在连续扫描的 15 天中,**所有 110 个检测到的交易环都与已确认的案例相似** — 因此分析师打开一个已经分类的工作清单。选择一个范围或日期字段;点击某一列跳转到该天。

### 2. 记忆变得更锐利 — 重点
分析师确认一个方案;代理将其写入 CockroachDB。几天后,**同样的骗子以完全不同的方式再次出现** — 关键词过滤器可能会错过它。距离表显示记忆带来了什么:相同的方案从 **0.5051 → 0.3741** 的余弦距离。被抓,而且**更早**被抓。

### 3. 每个案例的证据 — 为什么这不是猜测
案例 C001:5 个钱包,351 笔交易**彼此之间**,代币 **#689 返回到组 114 次**。真正的销售会将 NFT 移交给新所有者;这些循环永远不会。按 **重放** 按钮以按区块顺序观看交易环的形成 — 真实的链上序列,而不是动画。

---
## 实际运行
针对实时 CockroachDB Cloud 集群,使用真实的 2022 LooksRare 数据:
| | |
|---|---|
| 交易数据 (`nft_trades`) | **83,901** |
| 每日检测运行(2023-02-06 → 02-20) | **15 个连续天** |
| 检测到的勾结案例 (`collusion_cases`) | **147** (37 个基础 + 110 个来自每日运行) |
| 每日运行的交易环与确认的案例匹配 | **110 / 110** |
| 攻略手册模式在矢量内存中 (`flagged_patterns`) | **37** |
| 分布式矢量索引 | `flagged_patterns_embedding_idx` — **在线** |
| 嵌入 | AWS Bedrock Titan 文本嵌入 V2,1024 维 — **在线** |
| 标志理由 | AWS Bedrock 聊天模型 — **在线** |
---
## 工作原理(管道)
1. **摄入** LooksRare 2022 交易数据从 Dune → CockroachDB。
2. **确认** 洗钱交易对通过对称往返启发式(交易对在每个方向上交易近乎相同的次数)+ Etherscan 上的资金关系检查。
3. **挖掘前兆**:对于每个确认的对,拉取完整的钱包历史并工程早期信号特征(从第一个资金 → 第一个交易的时间,资金源多样性,对手方集中度)。
4. **学习** 哪些前兆最好地预测最终确认(AWS SageMaker),将顶级信号转为攻略手册条目。
5. **记住**:将攻略手册条目(AWS Bedrock)嵌入 CockroachDB 的矢量索引中。
6. **检测** 新活动通过矢量相似性 + 规则阈值;代理用一个人类可读的解释来标记,引用最接近的已知先例。
7. **关闭循环**:人类批准/拒绝被写入 CockroachDB — 使代理随着时间推移而改进的记忆。
## 使用的 CockroachDB 工具
- **分布式矢量索引** — **在线**。`VECTOR(1024)` 列 + C-SPANN 索引在 `flagged_patterns` 上;每个分类都是一个相似性搜索。参见 [`sql/schema.sql`](sql/schema.sql)。
- **持久代理内存** — **在线**。`confirm()` / `reject()` 裁决是行,不是会话状态,因此改进在重启后仍然存在,并被每个代理实例共享。
- **云托管 MCP 服务器** — **在线**。“询问记忆”面板以纯英语回答分析师问题;代理读取模式并在托管的 MCP 端点上运行只读 `select_query`,然后显示生成的 SQL。它从不为此路径打开自己的 SQL 连接,并且不持有写凭证 — [`src/crdb_mcp.py`](src/crdb_mcp.py) 拒绝任何不在只读允许列表中的工具,因为集群管理员密钥可以调用 `insert_rows`。通过 `python -m src.eval_ask` (8/8)检查准确性。
- **ccloud CLI** — _(拉伸)_ 集群配置/备份在演示中显示。
## 使用的 AWS 服务
- **Bedrock — Titan 文本嵌入 V2** — **在线**。将每个攻略手册条目和每个传入活动嵌入到 1024 维空间中,矢量索引搜索。
- **Bedrock — 聊天模型** — **在线**。编写引用最接近先例的普通语言理由。默认为 Claude Haiku 4.5;参见 *Bedrock 模型访问*。
- **Lambda** — _(计划)_ 驱动 `src/daily_run.py` 在生产中的每日计时器。
- **SageMaker** — _(计划)_ 训练前兆信号模型,提取特征重要性。
- **S3** — _(计划)_ 原始响应/审计存档。
### Bedrock 模型访问
Bedrock 上的 Claude 需要每个 AWS 账户一次性 **“Anthropic 使用案例详情”** 表(Bedrock 控制台 → *模型访问*)。在获得批准之前,账户会收到 `ResourceNotFoundException`,因此 `src/bedrock.py` 自动回退到另一个 Bedrock 模型,UI 用**实际生成模型的** 标签显示理由 — 演示永远不会显示空框。可以使用 `BEDROCK_CHAT_MODEL` / `BEDROCK_FALLBACK_CHAT_MODEL` 重写任一模型。
---
## 设置
需要 Python 3.11+。
```bash
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env # 然后填写实际的 DUNE_API_KEY 和 ETHERSCAN_API_KEY
```
然后设置集群并加载数据:
```bash
python -m src.crdb_init # 创建模式 + 分布式矢量索引
python -m src.crdb_load # 将交易数据加载到 CockroachDB
python -m src.ring_detect # 查找闭环交易环 -> collusion_cases
python -m src.build_playbook # 通过 Bedrock 嵌入确认的案例 -> 矢量内存
python -m src.daily_run --date 2023-02-16 # 一个“今日收获”运行
```
## 运行(里程碑 1 — 本地信号管道)
```bash
# 1. 从 Dune 拉取顶级候选对(LooksRare 2022)。
# 如果您在 Dune UI 中从 sql/top_pairs.sql 创建了查询:
python -m src.dune_pull --query-id <您的查询 ID>
# 或尝试从 .sql 文件编程创建:
python -m src.dune_pull --sql sql/top_pairs.sql --name "top pairs"
# 2. 按往返对称性对对进行评分,并标记洗钱交易候选者。
python -m src.pair_heuristic
# 3. 通过 Etherscan 跟踪每个候选钱包的资金历史。
python -m src.etherscan_trace
```
输出位于 `data/raw/` (原始拉取)和 `data/processed/` (评分对,钱包特征)。
## 架构
设计重点:**证据和记忆生活在同一个数据库中。** 交易环从交易中检测出来,写入案例,嵌入,并针对每个过去的裁决进行搜索 — 而无需将关系存储同步到单独的矢量存储。
```mermaid
flowchart LR
A["Alchemy getNFTSales<br/>Etherscan"] --> D["交易环检测<br/>图循环 · networkx"]
D --> CC[("勾结案例<br/>+ nft_trades")]
CC -- "案例注释" --> EMB["Titan 嵌入 V2<br/>1024 维"]
EMB --> FP[("标记的模式<br/>VECTOR 1024<br/>分布式矢量索引")]
NEW["每日运行<br/>今日收获"] --> TR["分类<br/>余弦搜索 vs 内存"]
FP --> TR
TR --> LLM["Bedrock LLM<br/>普通语言理由"]
LLM --> AN["分析师<br/>确认 / 拒绝"]
AN -- "裁决成为一行" --> FP
ASK["询问记忆<br/>自然语言"] --> MCP["托管 MCP 服务器<br/>只读 SQL"]
MCP --> CC
classDef crdb fill:#1a2b3d,stroke:#5b9dff,color:#e6edf3
classDef aws fill:#3d2a1a,stroke:#ffb84d,color:#e6edf3
class CC,FP,MCP crdb
class EMB,LLM aws
```
<sub>蓝色 = CockroachDB Cloud · 琥珀色 = AWS Bedrock</sub>
**最重要的循环** 是虚线:每个分析师的裁决成为 `flagged_patterns` 中的一行,因此下一次分类搜索更大的,更锐利的记忆 — 而且由于它通过嵌入而不是关键词匹配,因此即使方案被完全用不同的词语描述,也能被抓住。
## 许可证
MIT — 参见 [LICENSE](LICENSE)。