flapskill
创建 V5 代币(0 税或税收,四档分配);USDT 买入、按数量或按比例卖出;做市刷量(每轮 5 买 5 卖,启动销毁 5 万枚,无 USDT 时卖回 funder 后继续,日志北京时间)。说「蝴蝶技能」触发。依赖 BNB Chain MCP。
Packaged view
This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.
Install command
npx @skill-hub/cli install openclaw-skills-flap-skills
Repository
Skill path: skills/flap-builder/flap-skills
创建 V5 代币(0 税或税收,四档分配);USDT 买入、按数量或按比例卖出;做市刷量(每轮 5 买 5 卖,启动销毁 5 万枚,无 USDT 时卖回 funder 后继续,日志北京时间)。说「蝴蝶技能」触发。依赖 BNB Chain MCP。
Open repositoryBest for
Primary workflow: Ship Full Stack.
Technical facets: Full Stack, Integration.
Target audience: everyone.
License: Unknown.
Original source
Catalog source: SkillHub Club.
Repository owner: openclaw.
This is still a mirrored public skill entry. Review the repository before installing into production workflows.
What it helps with
- Install flapskill into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
- Review https://github.com/openclaw/skills before adding flapskill to shared team environments
- Use flapskill for development workflows
Works across
Favorites: 0.
Sub-skills: 0.
Aggregator: No.
Original source / Raw SKILL.md
---
name: flapskill
displayName: 蝴蝶技能
version: 1.8.0
description: 创建 V5 代币(0 税或税收,四档分配);USDT 买入、按数量或按比例卖出;做市刷量(每轮 5 买 5 卖,启动销毁 5 万枚,无 USDT 时卖回 funder 后继续,日志北京时间)。说「蝴蝶技能」触发。依赖 BNB Chain MCP。
---
# 蝴蝶技能:创建代币、买入/卖出代币(USDT)
用户说「**蝴蝶技能**」即触发本技能。通过 FlapSkill 合约可**创建** V5 代币(0 税或税收;税收时代币可分配营销/持币分红/回购销毁/LP回流 四档税点,受益人 feeTo),或用 **USDT** 买入/卖出指定代币;买卖经 Portal 或 PancakeSwap 兑换,代币/USDT 转给调用者。
**USDT 合约地址(BSC)**:`0x55d398326f99059fF775485246999027B3197955`。
**前置条件:** 用户需已配置 [BNB Chain MCP](https://docs.bnbchain.org/showcase/mcp/skills/)(如已安装 `bnbchain-mcp-skill`),且 MCP 的 `env` 中已设置 `PRIVATE_KEY`,否则无法发送交易。
---
## 1. 合约接口
**合约地址**:`0x482970490d06fc3a480bfd0e9e58141667cffedc`。
### 创建代币 createToken
- **方法**:`createToken(string _name, string _symbol, string _meta, address _feeTo, bytes32 _salt, uint16 _taxRate, uint16 _mktBps, uint16 _dividendBps, uint16 _deflationBps, uint16 _lpBps, uint256 _minimumShareBalance) external returns (address token)`
- **含义**:经 Portal 创建 V5 代币。**0 税代币**:`_taxRate=0`,不校验四档分配,合约使用 V3_MIGRATOR;**salt 须用尾号 8888**(脚本:`node scripts/find-vanity-salt.js 8888`)。**税收代币**:`_taxRate` 1–1000(如 300=3%),`_mktBps`(营销)、`_dividendBps`(持币分红)、`_deflationBps`(回购销毁)、`_lpBps`(LP 回流)四者之和须为 10000;**salt 须用尾号 7777**(全营销用 `find-vanity-salt.js 7777`,四档分配用 `find-vanity-salt.js 7777 v2`)。若 `_dividendBps > 0`,`_minimumShareBalance` 须 ≥ 10_000 ether;用户未说时 Agent 默认 10_000 ether。详见 [Flap Portal](https://docs.flap.sh/flap/developers/token-launcher-developers/launch-token-through-portal) 与 [部署地址](https://docs.flap.sh/flap/developers/token-launcher-developers/deployed-contract-addresses)(standard:8888,Tax:7777)。无需 approve。
- **约束**:`_salt` 为 bytes32(0x+64 位十六进制),每次创建用不同 salt;**必须按税点选对尾号(0 税→8888,有税→7777)及对应脚本 impl(7777 四档时加 v2)**。
- **何时用**:**0 税**:用户说「蝴蝶技能 创建代币 名称:… 符号:…」即可,可选官网、简介、代币图片;**不需说税点、税收地址**,Agent 自动 taxRate=0、feeTo=调用者地址、salt 用 8888。**有税**:用户说「蝴蝶技能 创建代币 名称:… 符号:…,税点:…% 税收地址:0x…」并可选四档分配、官网、简介、图片;有税且未指定四档时默认全部归营销;启用持币分红时用户可说「最低持币数量:1 万」等,不说则默认 10_000 ether。**_meta、_salt 由 Agent 跑脚本填入;官网、简介须传入 upload 脚本。**
### 买入 buyTokens
- **方法**:`buyTokens(address _token, uint256 _usdtAmount) external`
- **含义**:从调用者转入 `_usdtAmount`(18 位小数)的 USDT,向 Portal 兑换 `_token` 代币,代币转给调用者。
- **约束**:调用前须对 FlapSkill 合约 approve 至少 `_usdtAmount` 的 USDT。
### 卖出(按数量)sellTokens
- **方法**:`sellTokens(address _token, uint256 _tokenAmount) external`
- **含义**:从调用者转入指定数量 `_tokenAmount` 的代币,向 Portal 兑换为 USDT,USDT 转给调用者。无滑点保护。
- **约束**:调用前须对 FlapSkill 合约 approve 至少 `_tokenAmount` 的该代币。`_tokenAmount` 为该代币最小单位。
- **何时用**:用户说「蝴蝶技能卖出 X 个 0x…」「卖出100个的0x…」等**具体数量**时,用本方法。
### 卖出(按仓位比例)sellTokensByPercent
- **方法**:`sellTokensByPercent(address _token, uint256 _percentBps) external`
- **含义**:按调用者当前该代币持仓的 **比例** 卖出。合约内读取 `balanceOf(msg.sender)`,卖出数量 = 余额 × `_percentBps` / 10000。无滑点保护。
- **约束**:调用前须对 FlapSkill 合约 approve 至少「该比例对应的数量」(建议直接 approve 全部仓位或足够大的数)。
- **何时用**:用户说「蝴蝶技能卖出50%的0x…」「卖出一半 0x…」等**比例**时,用本方法。`_percentBps` 为基点:10000=100%,5000=50%,1000=10%。
---
## 2. 创建代币:使用 BNB Chain MCP 调用
### 2.1 _meta 与 _salt 由 Agent 直接填写(脚本已打包在本技能内)
- **\_meta** 和 **\_salt** 不由用户提供,**由 Agent 在技能目录下执行本技能自带的脚本得到并直接填入** createToken 的 args。
- **脚本位置**:本技能目录下的 `scripts/`(`upload-token-meta.js`、`find-vanity-salt.js`)。安装后技能目录通常为 `.agents/skills/flap-skills`(项目)或 `~/.cursor/skills/flap-skills`(全局),本仓库内为 `skills/flap-skills`。**Agent 须先 cd 到该技能目录**,若未安装依赖则先执行 `npm install`,再执行下面两步。
- 上传 meta 时须带上**官网**和**简介**(见 [Flap 文档](https://docs.flap.sh/flap/developers/token-launcher-developers/launch-token-through-portal)):用户创建代币时可选提供「官网:…」「简介:…」,Agent 传入脚本,格式为 `node scripts/upload-token-meta.js <图片路径> "<简介>" "<官网>" [twitter] [telegram]`,脚本将简介写入 meta.description、官网写入 meta.website。
- **Salt 尾号与脚本**(见 [Flap 文档](https://docs.flap.sh/flap/developers/token-launcher-developers/launch-token-through-portal#3-find-the-salt-vanity-suffix)、[部署地址](https://docs.flap.sh/flap/developers/token-launcher-developers/deployed-contract-addresses)):**0 税**(税点 0%)→ 地址尾号 **8888**,执行 `node scripts/find-vanity-salt.js 8888`;**有税**→ 地址尾号 **7777**,全营销执行 `node scripts/find-vanity-salt.js 7777`,四档分配执行 `node scripts/find-vanity-salt.js 7777 v2`。
- 流程:**0 税**:用户只说「蝴蝶技能 创建代币 名称:… 符号:…」(可选官网、简介、图片)。Agent 在技能目录下:① 上传 meta(有图/官网/简介时跑 upload 脚本,否则可用占位 CID 或简单 meta);② 执行 `node scripts/find-vanity-salt.js 8888` 得 _salt;③ _feeTo = 调用者地址(发送交易的钱包),_taxRate=0,四档与 minimumShareBalance 均 0;④ `write_contract` createToken(…)。**有税**:用户说「名称:… 符号:…,税点:…% 税收地址:0x…」并可选四档、官网、简介、图片。Agent:① 跑 upload 脚本得 _meta;② 按税点跑 salt(全营销 7777,四档 7777 v2);③ 确定四档与 minimumShareBalance;④ `write_contract` createToken(…)。
- 用户**无需**写 meta、salt;0 税**无需**说税点、税收地址,Agent 用调用者地址作 feeTo。
### 2.2 调用 createToken
无需 approve,直接 **`write_contract`**:
| 参数 | 说明 |
|------|------|
| `contractAddress` | `0x482970490d06fc3a480bfd0e9e58141667cffedc` |
| `abi` | createToken 的 ABI(见 [references/contract-abi.md](references/contract-abi.md)) |
| `functionName` | `"createToken"` |
| `args` | `[_name, _symbol, _meta, _feeTo, _salt, _taxRate, _mktBps, _dividendBps, _deflationBps, _lpBps, _minimumShareBalance]`。**0 税**:_feeTo=调用者地址,_taxRate=0,四档与 minimumShareBalance 填 0,_salt 用 8888。**有税**:_feeTo=用户提供的税收地址,_taxRate 1–1000,四档之和 10000,_salt 用 7777 或 7777 v2。**_meta、_salt 由 Agent 跑脚本填入。** |
| `network` | 可选,默认 `bsc` |
---
## 3. 买入:使用 BNB Chain MCP 调用
### 3.1 先授权 USDT(approve)
| 参数 | 说明 |
|------|------|
| `tokenAddress` | USDT:`0x55d398326f99059fF775485246999027B3197955` |
| `spenderAddress` | FlapSkill:`0x482970490d06fc3a480bfd0e9e58141667cffedc` |
| `amount` | 要支付的 USDT 数量(人类可读如 `"0.01"`) |
| `network` | 可选,默认 `bsc` |
### 3.2 再调用 buyTokens
| 参数 | 说明 |
|------|------|
| `contractAddress` | FlapSkill:`0x482970490d06fc3a480bfd0e9e58141667cffedc` |
| `abi` | buyTokens 的 ABI(见 [references/contract-abi.md](references/contract-abi.md)) |
| `functionName` | `"buyTokens"` |
| `args` | `[_token, _usdtAmount]`:目标代币地址、USDT 最小单位(18 位小数,如 0.01 USDT = `"10000000000000000"`) |
| `network` | 可选,默认 `bsc` |
---
## 4. 卖出(按数量):使用 BNB Chain MCP 调用
用户说**具体数量**(如「蝴蝶技能卖出100个的0x…」)时用此流程。
### 4.1 先授权要卖出的代币(approve)
| 参数 | 说明 |
|------|------|
| `tokenAddress` | 要卖出的代币合约地址 |
| `spenderAddress` | FlapSkill:`0x482970490d06fc3a480bfd0e9e58141667cffedc` |
| `amount` | 要卖出的代币数量(人类可读或最小单位,≥ 本次 `_tokenAmount`) |
| `network` | 可选,默认 `bsc` |
### 4.2 再调用 sellTokens
| 参数 | 说明 |
|------|------|
| `contractAddress` | FlapSkill:`0x482970490d06fc3a480bfd0e9e58141667cffedc` |
| `abi` | sellTokens 的 ABI(见 [references/contract-abi.md](references/contract-abi.md)) |
| `functionName` | `"sellTokens"` |
| `args` | `[_token, _tokenAmount]`:代币地址、卖出数量(该代币最小单位) |
| `network` | 可选,默认 `bsc` |
**注意**:`_tokenAmount` 须按该代币 decimals 换算为最小单位(如 18 位小数,1 个 = `"1000000000000000000"`)。
---
## 5. 卖出(按仓位比例):使用 BNB Chain MCP 调用
用户说**比例**(如「蝴蝶技能卖出50%的0x…」「卖出一半0x…」)时用此流程。
### 5.1 可选:查询用户该代币余额
用 **`get_erc20_balance`**(tokenAddress=代币,address=用户)得到余额,便于确认或向用户展示。合约内部会再次按当前区块状态计算比例,无需用此结果参与合约参数。
### 5.2 授权代币(approve)
建议对 FlapSkill approve **至少对应比例的数量**,或直接 approve 全部仓位(如 `amount` 用很大或「max」)。例如卖 50% 时,approve 至少当前余额的 50% 或全部。
| 参数 | 说明 |
|------|------|
| `tokenAddress` | 要卖出的代币合约地址 |
| `spenderAddress` | FlapSkill:`0x482970490d06fc3a480bfd0e9e58141667cffedc` |
| `amount` | 建议 ≥ 本次要卖出的数量(可按比例算,或直接填足够大) |
| `network` | 可选,默认 `bsc` |
### 5.3 调用 sellTokensByPercent
| 参数 | 说明 |
|------|------|
| `contractAddress` | FlapSkill:`0x482970490d06fc3a480bfd0e9e58141667cffedc` |
| `abi` | sellTokensByPercent 的 ABI(见 [references/contract-abi.md](references/contract-abi.md)) |
| `functionName` | `"sellTokensByPercent"` |
| `args` | `[_token, _percentBps]`:代币地址、比例基点(10000=100%,5000=50%,1000=10%) |
| `network` | 可选,默认 `bsc` |
**比例换算**:用户说 50% → `_percentBps` = `"5000"`;10% → `"1000"`;100% → `"10000"`。
---
## 6. 做市/刷量(与创建代币、买卖一致:用户说一句,Agent 自主执行)
做市与**创建代币、买卖**相同:用户只需说一句自然语言,**Agent 自主**用 MCP 钱包完成链上授权与登记并**启动做市脚本**,无需用户再手动发交易或跑命令。
**约定**:资金方(funder)= **MCP 当前发交易的钱包**(即 MCP 中 `PRIVATE_KEY` 对应地址)。**做市由 MCP 授权给技能合约**(对 FlapSkill approve USDT + setAllowedCallers 登记 worker),**由 worker 调用合约** buyForCaller/sellForCaller 完成买卖,**不是 MCP 直接买卖**。做市 **只有启动与停止**,不设磨损上限。Agent 在技能目录执行 `node scripts/mm-generate-workers.js` 生成 2~20 个随机 worker 密钥并**按生成时间命名导出到本地**(`mm-workers-<YYYYMMDD>-<HHmmss>.json`),**自主**通过 MCP 的 **transfer_native_token** 向每个 worker 地址**逐笔转 0.001 BNB 作 Gas**(**由 Agent 自主完成,用户无需手动转 BNB**),然后 MCP 对 FlapSkill **approve USDT**、**setAllowedCallers**,并启动 mm-bot(worker 私钥从该文件读取,由 worker 调用合约买卖)。**用户无需提供 worker 地址、私钥或手动转 BNB**,只说一句即可刷量。**做市过程中若某 worker 出现 Gas 不足**,Agent **自主**使用 MCP **transfer_native_token** 向该 worker(或 mm-bot 日志中提示的 buyer/seller 地址)转 0.001 BNB 补 gas,**无需主人批准**,视为做市授权的一部分。
### 6.1 自然语言示例(做市/刷量)
用户说一句,Agent **自主**解析并**依次执行**:生成 worker → **Agent 自主用 MCP transfer_native_token 向每个 worker 发 0.001 BNB 作 Gas** → **MCP 对 FlapSkill 合约 approve USDT(授权合约可划转 funder 的 USDT)、setAllowedCallers 登记 worker** → 在技能目录启动 `mm-bot.js`(**由 worker 调用合约** buyForCaller/sellForCaller 买卖,不是 MCP 直接买卖)。**做市只有启动与停止**,不会因磨损自动停止。
**示例:**
```
使用蝴蝶技能对 0xe139ca52ffd33d7cbb0dfeaf075f943c13937777 进行做市刷量,随机范围:1-10U,资金归集地址:0x62F5cCb8b1744A427b7511374F4eb33114217199
```
**参数对应:**
| 用户说法 | 含义 | 对应操作 / 配置 |
|----------|------|------------------|
| 对 **0x…** 进行做市刷量 | 代币合约地址 | `TOKEN_CA`、setAllowedCallers 的 token 参数 |
| **提供资金:100U** | 用于做市的资金规模(参考;授权采用无限授权) | MCP 对 FlapSkill **无限授权** USDT(amount = max uint256,见下文) |
| **随机范围:1-10U**(或 成交金额:1-10U) | 每笔买卖的 USDT 随机范围 | `USDT_MIN=1`,`USDT_MAX=10` |
| **资金归集地址:0x…** | **必填**。做市停止后 worker 剩余代币与 BNB 归集到该地址 | 启动 mm-bot 时设置 `COLLECT_TO_ADDRESS`;**只有用户说「停止做市刷量」或 Ctrl+C 会停止**,停止后自动执行归集 |
做市时 **MCP(funder)对 FlapSkill 合约** 的 USDT 授权采用**无限授权**:amount = max uint256,即 `"115792089237316195423570985008687907853269984665640564039457584007913129639935"`。若当前 MCP/viem 传该值时报溢出(如内部按 10^18 换算导致超范围),可改用大数值如 `"1000000000000000000000000000"`(10^27)作为回退。避免 worker 调用 buyForCaller 时因额度用尽报 TRANSFER_FROM_FAILED。**资金归集地址为必填**:用户必须提供「资金归集地址:0x…」,否则 Agent 不启动 mm-bot 并提示用户补充。**不设磨损上限**:合约已去掉磨损逻辑,做市只有启动与停止。未说「随机范围」/「成交金额」时用脚本默认(如 0.005~0.02 U)。未说「提供资金」时由 Agent 询问或按常见值(如 100 U)建议。**worker 由 Agent 自动生成**,无需用户配置。
### 6.2 合约接口(skill.sol)
- **setAllowedCallers(address _token, address[] _callers)**:由 **funder** 调用,登记对该 `_token` 允许调用 buyForCaller/sellForCaller 的地址(做市脚本的 worker 地址)。**调用时合约会从 funder 划转 5 万枚该代币到 0x0000…dEaD 销毁**,故调用前 funder 须对该 `_token` 向 FlapSkill approve 至少 5 万枚(`"50000"`)。小明对 0x0123 刷量前,小明须先 approve 0x0123 代币 5 万枚再调用 setAllowedCallers(0x0123, [小明的 worker 地址...])。
- **buyForCaller(address _token, uint256 _usdtAmount, address _funder)**:仅当 msg.sender 已被 _funder 对该 _token 登记时可调用;用 _funder 的 USDT 买入,代币转给调用者。调用前 _funder 须对 FlapSkill approve USDT。
- **sellForCaller(address _token, uint256 _tokenAmount, address _funder)**:仅当 msg.sender 已被 _funder 对该 _token 登记时可调用;调用者交出代币卖出,所得 USDT 转给 _funder。
- **removeAllowedCallers(address _token, address[] _callers)**:funder 取消某 token 下部分地址的调用权限。
ABI 见 [references/contract-abi.md](references/contract-abi.md)。
### 6.3 脚本与运行方式
- **生成 worker**:本技能目录下 `node scripts/mm-generate-workers.js [N]`(默认 N=20)生成 N 个随机 worker 私钥与地址,**按生成时间命名导出**为 `mm-workers-<YYYYMMDD>-<HHmmss>.json`。加 `--json` 时 stdout 输出 `{ "addresses": [...], "file": "<绝对路径>" }`,供 setAllowedCallers 用 `addresses`、启动 mm-bot 时用 `file` 作为 `PRIVATE_KEYS_FILE`。Agent 自主执行时先运行此脚本得到 worker 地址列表与导出文件路径。
- **做市脚本**:`scripts/mm-bot.js`(需在技能目录先 `npm install`)。支持从环境变量 `PRIVATE_KEYS` 或从文件 `PRIVATE_KEYS_FILE=<路径>`(由 mm-generate-workers 生成的按时间命名的文件)读取 worker 私钥。Agent 在**自主执行**时先完成链上三步(见 6.5),再运行 mm-bot。
- **流程**:每轮地址 A 调 **buyForCaller**(token, amount, funder) → A 将代币转给 B → B 调 **sellForCaller**(token, balance, funder)。金额在 [USDT_MIN, USDT_MAX] 内随机。
### 6.4 配置(环境变量或 CLI 参数)
| 配置项 | 环境变量 | CLI 参数位置 | 说明 |
|--------|----------|--------------|------|
| 资金方地址(funder) | `FUNDER_ADDRESS` | — | 必填,MCP 钱包地址,须已对 FlapSkill approve USDT |
| **资金归集地址** | `COLLECT_TO_ADDRESS` | — | **必填**,停止时将 worker 剩余代币与 BNB 归集到该地址 |
| 交易地址私钥 | `PRIVATE_KEYS` 或 `PRIVATE_KEYS_FILE` | — | 二选一:逗号分隔的 2~20 个私钥,或指向按时间命名的导出文件路径(mm-workers-YYYYMMDD-HHmmss.json);worker 仅需 BNB 作 gas |
| 代币地址 | `TOKEN_CA` | 第 1 个 | 必填 |
| 每笔 USDT 下限 | `USDT_MIN` | 第 2 个 | 默认 0.005,与上限之间随机 |
| 每笔 USDT 上限 | `USDT_MAX` | 第 3 个 | 默认 0.02 |
| 间隔(秒) | `INTERVAL_SEC` | 第 4 个 | 默认 15 |
| 轮数(0=无限) | `ROUNDS` | 第 5 个 | 默认 0 |
| RPC | `RPC_URL` | — | 默认 BSC 公网 |
示例:
```bash
# 前置:MCP(funder)对 FlapSkill 合约 approve USDT 并 setAllowedCallers;买卖由 worker 调用合约完成。worker 私钥二选一:
# 方式一:Agent 已生成 worker 导出文件时(文件名 mm-workers-YYYYMMDD-HHmmss.json)
export FUNDER_ADDRESS=0x... # MCP 钱包地址
export PRIVATE_KEYS_FILE=./mm-workers-20260304-220057.json # 或脚本 --json 输出中的 file 路径
export TOKEN_CA=0x...
node scripts/mm-bot.js
# 方式二:自行提供私钥
export FUNDER_ADDRESS=0x...
export PRIVATE_KEYS=0xkey1,0xkey2,...,0xkey20
export TOKEN_CA=0x...
export USDT_MIN=0.005
export USDT_MAX=0.02
node scripts/mm-bot.js
```
### 6.5 Agent 自主执行流程(与创建代币、买卖一致)
**做市刷量流程概览**:① 解析参数(代币、资金、随机范围、**资金归集地址**)→ ② 生成 worker 并导出文件 → ③ **Agent 自主用 MCP transfer_native_token 向每个 worker 转 0.001 BNB(Gas)** → ④ **MCP(funder)对 FlapSkill approve USDT(无限)** → ⑤ **MCP 对做市代币 approve 5 万枚给 FlapSkill** → ⑥ **MCP 调用 setAllowedCallers**(合约内将 5 万枚该代币从 funder 转入 0x0000…dEaD 销毁并登记 worker)→ ⑦ 启动 mm-bot(**worker 调用合约** buyForCaller/sellForCaller 买卖)。**只有用户说「停止做市刷量」或 Ctrl+C 会停止**,不会因磨损自动停止。
当用户说「蝴蝶技能 对 0x… 做市刷量」「使用蝴蝶技能对 0x… 进行做市刷量,提供资金:…,随机范围:…,资金归集地址:0x…」等时,Agent **自主**完成以下步骤(无需用户再手动发交易或跑脚本):
1. **解析自然语言**:从用户句中提取「代币地址」「提供资金(U)」「随机范围/成交金额(最小-最大 U)」「**资金归集地址**(必填)」。**不解析磨损**,做市不设磨损上限。**若用户未提供资金归集地址,则提示:「请提供资金归集地址(做市停止后 worker 剩余资金将归集到该地址)」,不执行后续步骤。** 未说提供资金时询问或建议(如 100 U);未说随机范围时用脚本默认。
2. **生成 worker**:在**技能目录**执行 `node scripts/mm-generate-workers.js [N] --json`(默认 N=20),脚本按生成时间命名将私钥与地址导出到本地(如 `mm-workers-20260304-220057.json`);stdout 输出 `{ "addresses": [...], "file": "<绝对路径>" }`,用 `addresses` 做 setAllowedCallers、用 `file` 做 PRIVATE_KEYS_FILE。若本次做市希望复用已有导出文件,可跳过本步并直接使用该文件的路径与其中的 `addresses`。
3. **给 worker 发 Gas(必须由 Agent 自主执行,不可省略或交给用户)**:Agent **自主**使用 MCP 的 **transfer_native_token**,向步骤 2 得到的每个 worker 地址**逐笔**转 **0.001 BNB**,供 worker 调用 buyForCaller/sellForCaller 时付 gas。共 N 笔(N = worker 数量,如 20)。**此步必须由 Agent 在启动 mm-bot 前完成**,用户无需操作。
**MCP 调用参数**:`toAddress`(worker 地址,注意参数名为 toAddress 而非 to)、`amount`:`"0.001"`、`network`:`"bsc"`。若某笔报错 replacement transaction underpriced,可间隔数秒后重试该笔。
4. **获取 funder**:funder 地址 = MCP 当前发交易使用的钱包地址(即 `PRIVATE_KEY` 对应地址)。
5. **链上三步(MCP 用 funder 钱包调用,仅授权给合约、登记 worker,不做买卖)**:
- ① **approve_token_spending**:token=USDT(`0x55d398326f99059fF775485246999027B3197955`),spender=FlapSkill(`0x482970490d06fc3a480bfd0e9e58141667cffedc`),amount=**无限授权** max uint256:`"115792089237316195423570985008687907853269984665640564039457584007913129639935"`;若 MCP/viem 报溢出则回退为 `"1000000000000000000000000000"`。即 **MCP 授权给技能合约**,允许合约从 funder 划转 USDT。
- ② **approve_token_spending**:token=**做市代币地址**(本次做市的 `TOKEN_CA`),spender=FlapSkill,amount=`"50000"`(5 万枚;合约在 setAllowedCallers 内会将该数量从 funder 转入 0x0000…dEaD 销毁)。
- ③ **write_contract**:contractAddress=FlapSkill,abi=setAllowedCallers 的 ABI,functionName=`"setAllowedCallers"`,args=`[代币地址, worker地址数组]`,network=`"bsc"`。登记后 **worker 才能调用** buyForCaller/sellForCaller;同时会执行上述 5 万枚销毁。
- ④ 无需 setMaxWear(合约已去掉磨损逻辑)。做市**只有启动与停止**,不会因磨损自动停止。**买卖由 mm-bot 用 worker 私钥调用合约完成**,不是 MCP 直接买卖。
6. **启动做市脚本**:在**技能目录**下执行(可后台运行):
`FUNDER_ADDRESS=<funder地址> PRIVATE_KEYS_FILE=<上一步生成的文件绝对路径> TOKEN_CA=<代币> COLLECT_TO_ADDRESS=<用户提供的资金归集地址> USDT_MIN=<解析出的下限> USDT_MAX=<解析出的上限> node scripts/mm-bot.js`
其中 PRIVATE_KEYS_FILE 使用步骤 2 中 `--json` 输出里的 `file` 字段;**COLLECT_TO_ADDRESS 必填**(用户句中「资金归集地址:0x…」)。并告知用户:做市已在后台运行,**只有说「停止做市刷量」或 Ctrl+C 会停止**;停止后会自动将 worker 剩余代币与 BNB 归集到指定地址;资金留在你的 MCP 钱包,卖回 USDT 会自动回到该钱包。
7. **做市过程中 worker Gas 不足时**:若用户反馈或日志出现「可能 worker Gas 不足」「[Agent 自主补 Gas 无需主人批准]」等提示(mm-bot 会输出可能缺 gas 的 buyer/seller 地址),Agent **自主**使用 MCP **transfer_native_token**(toAddress=该 worker 地址,amount=`"0.001"`,network=`"bsc"`)向该地址转 0.001 BNB,**无需再向主人确认或批准**,补足后做市可继续。
---
## 7. 流程简述
- **创建代币**:**0 税**:用户只说「蝴蝶技能 创建代币 名称:… 符号:…」;Agent 用调用者地址作 feeTo,taxRate=0,四档与 minimumShareBalance 填 0,salt 用 8888。**有税**:用户需说税点、税收地址;taxRate 1–1000,四档之和 10000,salt 用 7777 或 7777 v2;启用分红时 minimumShareBalance ≥ 10_000 ether。无需 approve。
- **买入**:approve USDT → `write_contract` buyTokens(`token`, `usdtAmount`)。
- **卖出(按数量)**:用户说具体数量 → approve 代币 → `write_contract` sellTokens。
- **卖出(按比例)**:用户说比例 → 可选 get_erc20_balance → approve 代币 → `write_contract` sellTokensByPercent。
- **做市/刷量**:用户说「对 0x… 做市刷量,提供资金:…,随机范围:…,**资金归集地址:0x…**」→ Agent 自主生成 worker、**自主向每个 worker 转 0.001 BNB(Gas)**、**MCP 对技能合约无限授权 USDT 并 setAllowedCallers**、再启动 mm-bot(**由 worker 调用合约买卖**,**COLLECT_TO_ADDRESS 必填**)。**只有「停止做市刷量」或 Ctrl+C 会停止**,停止时自动将 worker 剩余资金归集到该地址。**做市过程中若 worker Gas 不足**,Agent **自主**用 MCP transfer_native_token 向该 worker 转 0.001 BNB,**无需主人批准**。
- 发送前向用户确认合约地址、代币/参数和金额/比例。
---
## 8. 参考
- **createToken / buyTokens / sellTokens / sellTokensByPercent ABI**:[references/contract-abi.md](references/contract-abi.md)
- **获取 _meta / _salt**:脚本已打包在本技能 `scripts/` 下(upload-token-meta.js、find-vanity-salt.js),在技能目录执行并先 `npm install`。另本仓库根目录 [scripts/README.md](../../scripts/README.md) 有相同脚本与说明。
- **BNB Chain MCP / Skills**:[BNB Chain Skills](https://docs.bnbchain.org/showcase/mcp/skills/)
- **本仓库合约**:`skill.sol` 中 `FlapSkill`(createToken、buyTokens、sellTokens、sellTokensByPercent、**buyForCaller**、**sellForCaller** 做市/刷量)。做市脚本 `scripts/mm-bot.js`。
---
## Referenced Files
> The following files are referenced in this skill and included for context.
### references/contract-abi.md
```markdown
# 蝴蝶技能 createToken / buyTokens / sellTokens — ABI 与调用示例
**合约地址**:`0x482970490d06fc3a480bfd0e9e58141667cffedc`。
**USDT(BSC)**:`0x55d398326f99059fF775485246999027B3197955`。
---
## createToken 函数 ABI(创建 V5 代币)
```json
[
{
"inputs": [
{ "internalType": "string", "name": "_name", "type": "string" },
{ "internalType": "string", "name": "_symbol", "type": "string" },
{ "internalType": "string", "name": "_meta", "type": "string" },
{ "internalType": "address", "name": "_feeTo", "type": "address" },
{ "internalType": "bytes32", "name": "_salt", "type": "bytes32" },
{ "internalType": "uint16", "name": "_taxRate", "type": "uint16" },
{ "internalType": "uint16", "name": "_mktBps", "type": "uint16" },
{ "internalType": "uint16", "name": "_dividendBps", "type": "uint16" },
{ "internalType": "uint16", "name": "_deflationBps", "type": "uint16" },
{ "internalType": "uint16", "name": "_lpBps", "type": "uint16" },
{ "internalType": "uint256", "name": "_minimumShareBalance", "type": "uint256" }
],
"name": "createToken",
"outputs": [{ "internalType": "address", "name": "token", "type": "address" }],
"stateMutability": "nonpayable",
"type": "function"
}
]
```
**调用**:直接 `write_contract`,`functionName`: `"createToken"`,`args`: `[name, symbol, meta, feeTo, salt, taxRate, mktBps, dividendBps, deflationBps, lpBps, minimumShareBalance]`。**0 税代币**:taxRate=0,mktBps/dividendBps/deflationBps/lpBps/minimumShareBalance 均填 0;salt 须用尾号 8888(`find-vanity-salt.js 8888`)。**税收代币**:taxRate 1–1000(100=1%,300=3%);mktBps、dividendBps、deflationBps、lpBps 四者之和须为 10000;salt 须用尾号 7777(全营销用 7777,四档用 7777 v2);启用分红时 minimumShareBalance ≥ 10_000 ether(如 `"10000000000000000000000"`),否则 0。salt 为 bytes32(0x+64 位十六进制)。详见 Flap [Portal](https://docs.flap.sh/flap/developers/token-launcher-developers/launch-token-through-portal)、[部署地址](https://docs.flap.sh/flap/developers/token-launcher-developers/deployed-contract-addresses)。
---
## buyTokens 函数 ABI
```json
[
{
"inputs": [
{ "internalType": "address", "name": "_token", "type": "address" },
{ "internalType": "uint256", "name": "_usdtAmount", "type": "uint256" }
],
"name": "buyTokens",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]
```
**调用**:先 `approve_token_spending`(token=USDT,spender=FlapSkill,amount=本次 USDT);再 `write_contract`,`functionName`: `"buyTokens"`,`args`: `[目标代币地址, usdtAmount最小单位]`。例:10 USDT = `"10000000000000000000"`。
---
## sellTokens 函数 ABI
```json
[
{
"inputs": [
{ "internalType": "address", "name": "_token", "type": "address" },
{ "internalType": "uint256", "name": "_tokenAmount", "type": "uint256" }
],
"name": "sellTokens",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]
```
**调用**:先 `approve_token_spending`(token=要卖出的代币,spender=FlapSkill,amount=本次卖出数量);再 `write_contract`,`functionName`: `"sellTokens"`,`args`: `[代币地址, tokenAmount最小单位]`。`_tokenAmount` 须按该代币 decimals 换算(如 18 位小数,1 个 = `"1000000000000000000"`)。无滑点保护。USDT 直接转给调用者,无返回值。用于**按具体数量**卖出。
---
## sellTokensByPercent 函数 ABI(按仓位比例卖出)
```json
[
{
"inputs": [
{ "internalType": "address", "name": "_token", "type": "address" },
{ "internalType": "uint256", "name": "_percentBps", "type": "uint256" }
],
"name": "sellTokensByPercent",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "nonpayable",
"type": "function"
}
]
```
**调用**:先 `approve_token_spending`(token=要卖出的代币,spender=FlapSkill,amount 建议≥比例对应数量或全部仓位);再 `write_contract`,`functionName`: `"sellTokensByPercent"`,`args`: `[代币地址, percentBps]`。`_percentBps` 为基点:10000=100%,5000=50%,1000=10%。合约按用户当前持仓 × percentBps/10000 计算卖出数量。无滑点保护。用于**按仓位比例/百分比**卖出。
---
## buyForCaller 函数 ABI(做市/刷量:用 funder 的 USDT 买入,代币给调用者)
```json
[
{
"inputs": [
{ "internalType": "address", "name": "_token", "type": "address" },
{ "internalType": "uint256", "name": "_usdtAmount", "type": "uint256" },
{ "internalType": "address", "name": "_funder", "type": "address" }
],
"name": "buyForCaller",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]
```
**调用**:仅当 `allowedCallers[_funder][_token][msg.sender] == true` 时可调用(funder 须已对该 token 调用 `setAllowedCallers` 登记调用地址)。`_funder` 须已对 FlapSkill approve USDT。用于区分「小明对 0x0123 刷量」「小红对 0x456 刷量」等不同会话。
---
## sellForCaller 函数 ABI(做市/刷量:调用者交出代币卖出,USDT 给 funder)
```json
[
{
"inputs": [
{ "internalType": "address", "name": "_token", "type": "address" },
{ "internalType": "uint256", "name": "_tokenAmount", "type": "uint256" },
{ "internalType": "address", "name": "_funder", "type": "address" }
],
"name": "sellForCaller",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]
```
**调用**:仅当 `allowedCallers[_funder][_token][msg.sender] == true` 时可调用。调用者须先对 FlapSkill approve 代币。所得 USDT 转给 `_funder`。
---
## setAllowedCallers 函数 ABI(做市:登记该 token 下允许调用 buyForCaller/sellForCaller 的地址)
```json
[
{
"inputs": [
{ "internalType": "address", "name": "_token", "type": "address" },
{ "internalType": "address[]", "name": "_callers", "type": "address[]" }
],
"name": "setAllowedCallers",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]
```
**调用**:由 **funder(资金方)** 调用,`msg.sender` 即 funder。登记对该 `_token` 允许调用 buyForCaller/sellForCaller 的地址列表(做市脚本使用的 PRIVATE_KEYS 对应地址)。小明对 0x0123 刷量前,小明钱包调用 `setAllowedCallers(0x0123, [小明用的 worker 地址...])`;小红对 0x456 刷量前,小红钱包调用 `setAllowedCallers(0x456, [小红用的 worker 地址...])`,从而区分不同人、不同代币的会话。
---
## removeAllowedCallers 函数 ABI(取消某 token 下部分地址的调用权限)
```json
[
{
"inputs": [
{ "internalType": "address", "name": "_token", "type": "address" },
{ "internalType": "address[]", "name": "_callers", "type": "address[]" }
],
"name": "removeAllowedCallers",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]
```
**调用**:由 funder 调用,取消上述地址对该 token 的调用权限。
```
### scripts/mm-bot.js
```javascript
#!/usr/bin/env node
/**
* 做市/刷量机器人:资金在 MCP 钱包(funder),MCP 仅对 FlapSkill 合约授权 USDT 并 setAllowedCallers;
* 买卖由 worker 调用 FlapSkill.buyForCaller / sellForCaller 完成,不是 MCP 直接买卖。
*
* 环境变量:FUNDER_ADDRESS, TOKEN_CA 必填。私钥二选一:PRIVATE_KEYS 或 PRIVATE_KEYS_FILE。可选 COLLECT_TO_ADDRESS:停止时(用户停止或 Ctrl+C)自动将 worker 剩余代币与 BNB 归集到该地址。做市不设磨损上限,仅由用户停止。
* 若 MCP 钱包(funder)无 USDT 导致 buyForCaller 失败(TRANSFER_FROM_FAILED),则自动将所有 worker 持有的该代币通过 sellForCaller 卖给 funder(USDT 回到 funder),然后继续刷量,不停止。
*/
import { createPublicClient, createWalletClient, http, parseAbi, parseUnits, getAddress } from "viem";
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
import { execSync } from "child_process";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
import { privateKeyToAccount } from "viem/accounts";
import { bsc } from "viem/chains";
const FLAP_SKILL = getAddress("0x482970490d06fc3a480bfd0e9e58141667cffedc");
const USDT_DECIMALS = 18;
const MAX_WORKERS = 20;
/** 每轮同时买入/卖出的地址数:BATCH 个同时买,BATCH 个同时卖(买与卖地址不重合) */
const BATCH = 5;
const FLAP_SKILL_ABI = parseAbi([
"function buyForCaller(address _token, uint256 _usdtAmount, address _funder) external",
"function sellForCaller(address _token, uint256 _tokenAmount, address _funder) external",
]);
const ERC20_ABI = parseAbi([
"function approve(address spender, uint256 amount) external returns (bool)",
"function balanceOf(address account) external view returns (uint256)",
"function transfer(address to, uint256 amount) external returns (bool)",
]);
function env(key, def) {
const v = process.env[key];
return v !== undefined && v !== "" ? v : def;
}
function parseNum(s, def) {
if (s === undefined || s === "") return def;
const n = Number(s);
if (Number.isNaN(n)) return def;
return n;
}
function randomInRange(min, max) {
const m = Math.min(min, max);
const M = Math.max(min, max);
const r = m + Math.random() * (M - m);
return Math.round(r * 1e4) / 1e4;
}
/** 返回当前北京时间字符串,用于日志 */
function beijingTime() {
return new Date().toLocaleString("zh-CN", { timeZone: "Asia/Shanghai", hour12: false });
}
function parsePrivateKeys(input) {
if (!input || typeof input !== "string") return [];
return input
.split(",")
.map((s) => s.trim())
.filter(Boolean);
}
function loadWorkerKeys() {
const fromEnv = parsePrivateKeys(env("PRIVATE_KEYS", ""));
if (fromEnv.length >= 2) return fromEnv;
const filePath = env("PRIVATE_KEYS_FILE", "");
if (!filePath) return [];
const abs = path.isAbsolute(filePath) ? filePath : path.join(process.cwd(), filePath);
if (!fs.existsSync(abs)) return [];
const data = JSON.parse(fs.readFileSync(abs, "utf8"));
const keys = Array.isArray(data.privateKeys) ? data.privateKeys : [];
return keys.filter((k) => typeof k === "string" && k.length > 0);
}
async function main() {
const tokenCa = env("TOKEN_CA", process.argv[2]);
if (!tokenCa) {
console.error("用法: TOKEN_CA=0x... node scripts/mm-bot.js 或 node scripts/mm-bot.js <TOKEN_CA> [usdtMin] [usdtMax] [intervalSec] [rounds]");
console.error("必须: FUNDER_ADDRESS (MCP 钱包地址), TOKEN_CA");
console.error("私钥二选一: PRIVATE_KEYS (2~20 个逗号分隔) 或 PRIVATE_KEYS_FILE=.mm-workers.json(由 mm-generate-workers.js 生成)");
console.error("必填: COLLECT_TO_ADDRESS(资金归集地址)。可选: RPC_URL, USDT_MIN, USDT_MAX, INTERVAL_SEC, ROUNDS");
process.exit(1);
}
const token = getAddress(tokenCa);
const collectTo = env("COLLECT_TO_ADDRESS", "");
if (!collectTo) {
console.error("请设置环境变量 COLLECT_TO_ADDRESS(资金归集地址;停止时将 worker 剩余代币与 BNB 归集到该地址)");
process.exit(1);
}
getAddress(collectTo);
const rpcUrl = env("RPC_URL", "https://bsc-dataseed.binance.org");
const funderAddressRaw = env("FUNDER_ADDRESS", "");
if (!funderAddressRaw) {
console.error("请设置环境变量 FUNDER_ADDRESS(MCP 钱包地址,资金方;须已对 FlapSkill approve USDT)");
process.exit(1);
}
const funderAddress = getAddress(funderAddressRaw);
const workerKeys = loadWorkerKeys();
const minWorkers = 2 * BATCH;
if (workerKeys.length < minWorkers || workerKeys.length > MAX_WORKERS) {
console.error(`请设置 PRIVATE_KEYS 或 PRIVATE_KEYS_FILE(.mm-workers.json),需 ${minWorkers}~${MAX_WORKERS} 个 worker 私钥(当前每轮 ${BATCH} 个同时买、${BATCH} 个同时卖)`);
process.exit(1);
}
const usdtMin = parseNum(env("USDT_MIN", process.argv[3]), 0.005);
const usdtMax = parseNum(env("USDT_MAX", process.argv[4]), 0.02);
const intervalSec = parseNum(env("INTERVAL_SEC", process.argv[5]), 15);
const rounds = parseNum(env("ROUNDS", process.argv[6]), 0);
let collected = false;
const runCollectOnExit = () => {
if (collected) return;
collected = true;
const keysFile = env("PRIVATE_KEYS_FILE", "");
if (!keysFile) return;
console.log("正在将 worker 剩余资金归集到", collectTo, "...");
try {
execSync(`node "${path.join(__dirname, "mm-collect.js")}"`, {
env: { ...process.env, TARGET_ADDRESS: collectTo, TOKEN_CA: tokenCa },
cwd: process.cwd(),
stdio: "inherit",
});
} catch (e) {
console.error("归集失败:", e.message || e);
}
};
process.on("SIGINT", () => {
console.log("收到停止信号,做市将在本轮结束后停止并执行归集…");
stopRequested = true;
});
process.on("SIGTERM", () => {
console.log("收到停止信号,做市将在本轮结束后停止并执行归集…");
stopRequested = true;
});
const transport = http(rpcUrl);
const chain = bsc;
const publicClient = createPublicClient({ chain, transport });
const accounts = workerKeys.map((pk) => privateKeyToAccount(pk));
const walletClients = accounts.map((account) => createWalletClient({ account, chain, transport }));
const tokenContract = { address: token, abi: ERC20_ABI };
console.log("做市机器人配置:");
console.log(" FlapSkill:", FLAP_SKILL);
console.log(" 资金方(funder):", funderAddress);
console.log(" 代币:", token);
console.log(" 交易地址数:", accounts.length, `(每轮 ${BATCH} 个同时买、${BATCH} 个同时卖)`);
console.log(" 每笔 USDT 范围:", usdtMin, "~", usdtMax);
console.log(" 间隔(秒):", intervalSec, " 轮数(0=无限):", rounds);
console.log("---");
console.log("请确认已通过 MCP 用 funder 钱包:1) 对 FlapSkill approve USDT 2) 调用 setAllowedCallers(" + token + ", [上述 " + accounts.length + " 个地址]),以区分不同人、不同代币的刷量会话。");
console.log("---");
let done = 0;
let stopRequested = false;
const isGasRelatedError = (msg) => {
if (!msg || typeof msg !== "string") return false;
const s = msg.toLowerCase();
return (
s.includes("insufficient") ||
s.includes("exceeds the balance") ||
s.includes("not enough") ||
s.includes("balance of the account")
);
};
/** funder 无 USDT 导致 buyForCaller 扣款失败 */
const isFunderNoUsdtError = (msg) => {
if (!msg || typeof msg !== "string") return false;
const s = msg.toLowerCase();
return s.includes("transfer_from_failed") || s.includes("transferhelper");
};
/** 将当前所有 worker 持有的该代币通过 sellForCaller 卖给 funder,然后退出并归集 */
const runSellAllWorkersToFunder = async () => {
console.log(`[${beijingTime()}] [MCP 无 USDT] 正在将各 worker 持有的代币全部卖给 funder…`);
let soldCount = 0;
for (let i = 0; i < accounts.length; i++) {
const account = accounts[i];
const wallet = walletClients[i];
try {
const balance = await publicClient.readContract({
...tokenContract,
functionName: "balanceOf",
args: [account.address],
});
if (balance === 0n) continue;
const approveHash = await wallet.writeContract({
...tokenContract,
functionName: "approve",
args: [FLAP_SKILL, balance],
account,
});
await publicClient.waitForTransactionReceipt({ hash: approveHash });
const sellHash = await wallet.writeContract({
address: FLAP_SKILL,
abi: FLAP_SKILL_ABI,
functionName: "sellForCaller",
args: [token, balance, funderAddress],
account,
});
await publicClient.waitForTransactionReceipt({ hash: sellHash });
soldCount++;
console.log(` worker ${account.address.slice(0, 10)}… 已卖出,tx ${sellHash.slice(0, 10)}…`);
} catch (e) {
console.error(` worker ${account.address} 卖出失败:`, (e && e.message) || e);
}
}
console.log(`[${beijingTime()}] [MCP 无 USDT] 已将所有 worker 代币卖给 funder,共 ${soldCount} 笔。funder 已收回 USDT,继续刷量。`);
};
/** 打乱数组并取前 n 个 */
const shuffle = (arr, n) => {
const a = [...arr];
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
return a.slice(0, n);
};
const runOne = async () => {
const L = accounts.length;
const allIndices = [...Array(L).keys()];
const chosen = shuffle(allIndices, 2 * BATCH);
const buyerIndices = chosen.slice(0, BATCH);
const sellerIndices = chosen.slice(BATCH, 2 * BATCH);
const buyers = buyerIndices.map((i) => accounts[i]);
const sellerAccounts = sellerIndices.map((i) => accounts[i]);
const buyerWallets = buyerIndices.map((i) => walletClients[i]);
const sellerWallets = sellerIndices.map((i) => walletClients[i]);
try {
// 1) BATCH 个地址同时买入(并行发 tx,再等全部确认)
const buyAmounts = Array.from({ length: BATCH }, () => randomInRange(usdtMin, usdtMax));
const buyPromises = buyAmounts.map((amount, i) =>
buyerWallets[i].writeContract({
address: FLAP_SKILL,
abi: FLAP_SKILL_ABI,
functionName: "buyForCaller",
args: [token, parseUnits(String(amount), USDT_DECIMALS), funderAddress],
account: buyers[i],
})
);
const buyHashes = await Promise.all(buyPromises);
await Promise.all(buyHashes.map((hash) => publicClient.waitForTransactionReceipt({ hash })));
// 2) 读取 5 个买家的代币余额,再 BATCH 笔并行转给对应卖家(每人转 50%~100% 随机)
const buyerBalances = await Promise.all(
buyers.map((acc) =>
publicClient.readContract({
...tokenContract,
functionName: "balanceOf",
args: [acc.address],
})
)
);
const transferAmounts = buyerBalances.map((bal) => {
const ratioBps = 5000 + Math.floor(Math.random() * 5001);
let amt = (bal * BigInt(ratioBps)) / 10000n;
if (amt < 1n) amt = 1n;
return amt;
});
const transferPromises = transferAmounts.map((amt, i) =>
buyerWallets[i].writeContract({
...tokenContract,
functionName: "transfer",
args: [sellerAccounts[i].address, amt],
account: buyers[i],
})
);
const transferHashes = await Promise.all(transferPromises);
await Promise.all(transferHashes.map((hash) => publicClient.waitForTransactionReceipt({ hash })));
// 3) BATCH 个卖家同时授权并卖出(先并行 approve,再并行 sellForCaller)
const sellerBalances = await Promise.all(
sellerAccounts.map((acc) =>
publicClient.readContract({
...tokenContract,
functionName: "balanceOf",
args: [acc.address],
})
)
);
const approvePromises = sellerBalances.map((bal, i) =>
sellerWallets[i].writeContract({
...tokenContract,
functionName: "approve",
args: [FLAP_SKILL, bal],
account: sellerAccounts[i],
})
);
const approveHashes = await Promise.all(approvePromises);
await Promise.all(approveHashes.map((hash) => publicClient.waitForTransactionReceipt({ hash })));
const sellPromises = sellerBalances.map((bal, i) =>
sellerWallets[i].writeContract({
address: FLAP_SKILL,
abi: FLAP_SKILL_ABI,
functionName: "sellForCaller",
args: [token, bal, funderAddress],
account: sellerAccounts[i],
})
);
const sellHashes = await Promise.all(sellPromises);
await Promise.all(sellHashes.map((hash) => publicClient.waitForTransactionReceipt({ hash })));
const totalBuyU = buyAmounts.reduce((a, b) => a + b, 0).toFixed(2);
console.log(
`[${beijingTime()}] 轮 ${done + 1} ${BATCH} 买 ${totalBuyU} U | ${BATCH} 卖 | 买 ${buyHashes[0].slice(0, 8)}… 卖 ${sellHashes[0].slice(0, 8)}…`
);
done++;
} catch (e) {
const errMsg = (e && e.message) ? String(e.message) : "";
if (isFunderNoUsdtError(errMsg)) {
console.error(`[${beijingTime()}] 轮 ${done + 1} 失败: MCP 钱包(funder)无 USDT,无法扣款。`);
await runSellAllWorkersToFunder();
return;
}
if (isGasRelatedError(errMsg)) {
const addrs = [...buyerIndices.map((i) => accounts[i].address), ...sellerIndices.map((i) => accounts[i].address)];
console.error(
`[${beijingTime()}] 轮 ${done + 1} 失败(可能 worker Gas 不足):`,
errMsg.slice(0, 200)
);
console.error(
`[Agent 自主补 Gas] 请用 MCP transfer_native_token 向可能缺 Gas 的 worker 各转 0.001 BNB:`,
addrs.slice(0, 3).join(", "),
"..."
);
}
throw e;
}
};
const loop = async () => {
while (!stopRequested && (rounds === 0 || done < rounds)) {
try {
await runOne();
} catch (e) {
const errMsg = (e && e.message) ? String(e.message) : "";
if (!isGasRelatedError(errMsg)) {
console.error(`[${beijingTime()}] 轮 ${done + 1} 失败:`, errMsg || e);
}
}
if (stopRequested || (rounds > 0 && done >= rounds)) break;
await new Promise((r) => setTimeout(r, intervalSec * 1000));
}
console.log("做市结束,共执行", done, "轮");
};
await loop();
runCollectOnExit();
}
main().catch((e) => {
console.error(e);
process.exit(1);
});
```
### scripts/find-vanity-salt.js
```javascript
#!/usr/bin/env node
/**
* 计算 createToken 所需的 _salt(bytes32),使 CREATE2 部署出的代币地址满足 Portal 要求的 vanity 后缀。
* 文档:https://docs.flap.sh/flap/developers/token-launcher-developers/launch-token-through-portal
* - 税收代币 (tax):后缀 7777
* - 标准代币 (no tax):后缀 8888
*/
import { keccak256, toHex, getContractAddress, hexToBytes } from "viem";
import crypto from "crypto";
/**
* Portal / 实现地址见:https://docs.flap.sh/flap/developers/token-launcher-developers/deployed-contract-addresses
* Vanity 尾号见:https://docs.flap.sh/flap/developers/token-launcher-developers/launch-token-through-portal#3-find-the-salt-vanity-suffix
*/
const PORTAL_BSC = "0xe2cE6ab80874Fa9Fa2aAE65D277Dd6B8e65C9De0";
const STANDARD_TOKEN_IMPL_BSC = "0x8b4329947e34b6d56d71a3385cac122bade7d78d";
const TAX_TOKEN_V1_IMPL_BSC = "0x29e6383F0ce68507b5A72a53c2B118a118332aA8";
const TAX_TOKEN_V2_IMPL_BSC = "0xae562c6A05b798499507c6276C6Ed796027807BA";
function getInitCode(implAddress) {
const impl = implAddress.toLowerCase().replace(/^0x/, "").padStart(40, "0");
return ("0x3d602d80600a3d3981f3363d3d373d3d3d363d73" + impl + "5af43d82803e903d91602b57fd5bf3");
}
function predictTokenAddress(salt, tokenImpl, portal = PORTAL_BSC) {
const bytecode = getInitCode(tokenImpl);
const addr = getContractAddress({
from: portal,
salt: hexToBytes(salt),
bytecode,
opcode: "CREATE2",
});
return addr;
}
/**
* 根据税点与分配选择实现合约与尾号:
* - 0 税(standard):尾号 8888,Standard Token Impl
* - 税收且 mktBps==10000:尾号 7777,Tax Token V1 Impl
* - 税收且 mktBps<10000:尾号 7777,Tax Token V2 Impl
*/
function getImplAndSuffix(taxRate, mktBps = 10000) {
if (taxRate === 0) {
return { suffix: "8888", impl: STANDARD_TOKEN_IMPL_BSC };
}
if (mktBps >= 10000) {
return { suffix: "7777", impl: TAX_TOKEN_V1_IMPL_BSC };
}
return { suffix: "7777", impl: TAX_TOKEN_V2_IMPL_BSC };
}
/**
* 寻找使代币地址以 suffix 结尾的 salt。
* @param {string} suffix - 4 字符,如 "7777" 或 "8888"
* @param {string} tokenImpl - Portal 使用的 token 实现地址(8888 用 Standard,7777 用 Tax V1 或 V2)
*/
export function findVanitySalt(suffix = "7777", tokenImpl = TAX_TOKEN_V1_IMPL_BSC, portal = PORTAL_BSC) {
if (suffix.length !== 4) {
throw new Error("suffix 必须为 4 个字符,如 7777 或 8888");
}
const suffixLower = suffix.toLowerCase();
let salt = keccak256(toHex(crypto.randomBytes(32)));
let iterations = 0;
while (true) {
const addr = predictTokenAddress(salt, tokenImpl, portal);
if (addr.toLowerCase().endsWith(suffixLower)) {
return { salt, address: addr, iterations };
}
salt = keccak256(salt);
iterations++;
if (iterations % 100000 === 0) {
process.stdout.write(`\r已尝试 ${iterations} 次...`);
}
}
}
async function main() {
const args = process.argv.slice(2);
const suffix = (args[0] || "7777").toLowerCase();
const implArg = (args[1] || "").toLowerCase(); // "v2" 或 "standard" / "8888" 时选对应 impl
if (!/^[0-9a-f]{4}$/.test(suffix)) {
console.error("用法: node find-vanity-salt.js <suffix> [impl]");
console.error(" suffix: 8888 = 0 税标准代币(用 Standard Impl),7777 = 税收代币(用 Tax Impl)");
console.error(" impl: 可选。7777 时填 v2 表示 Tax V2 Impl(四档分配),不填默认 Tax V1 Impl(全营销)");
process.exit(1);
}
const tokenImpl =
suffix === "8888"
? STANDARD_TOKEN_IMPL_BSC
: implArg === "v2"
? TAX_TOKEN_V2_IMPL_BSC
: TAX_TOKEN_V1_IMPL_BSC;
console.log("正在计算 salt(后缀 " + suffix + ",impl: " + (suffix === "8888" ? "Standard" : implArg === "v2" ? "Tax V2" : "Tax V1") + "),请稍候…");
const { salt, address, iterations } = findVanitySalt(suffix, tokenImpl);
console.log("\n结果:");
console.log(" salt (bytes32):", salt);
console.log(" 预测代币地址: ", address);
console.log(" 迭代次数: ", iterations);
}
main().catch((err) => {
console.error(err.message || err);
process.exit(1);
});
```
### scripts/upload-token-meta.js
```javascript
#!/usr/bin/env node
/**
* 上传代币元数据到 Flap API,获取 IPFS CID 作为 createToken 的 _meta。
* 文档:https://docs.flap.sh/flap/developers/token-launcher-developers/launch-token-through-portal
* API:https://funcs.flap.sh/api/upload(必须用此 API 固定到 Flap 网关,索引才能拉取)
*/
import axios from "axios";
import FormData from "form-data";
import fs from "fs";
import path from "path";
const FLAP_UPLOAD_API = "https://funcs.flap.sh/api/upload";
const MUTATION_CREATE = `
mutation Create($file: Upload!, $meta: MetadataInput!) {
create(file: $file, meta: $meta)
}
`;
/**
* 上传代币元数据到 Flap API(含官网、简介等),见 https://docs.flap.sh/flap/developers/token-launcher-developers/launch-token-through-portal
* @param {Object} opts
* @param {string} opts.imagePath - 图片本地路径(如 logo.png)
* @param {string} [opts.description] - 简介/代币描述(提交到 meta.description)
* @param {string} [opts.website] - 官网(提交到 meta.website)
* @param {string} [opts.twitter]
* @param {string} [opts.telegram]
* @param {string} [opts.creator] - 创作者地址,默认零地址
* @returns {Promise<string>} IPFS CID,即 _meta
*/
export async function uploadTokenMeta(opts) {
const {
imagePath,
description = "",
website = null,
twitter = null,
telegram = null,
creator = "0x0000000000000000000000000000000000000000",
} = opts;
const form = new FormData();
form.append(
"operations",
JSON.stringify({
query: MUTATION_CREATE,
variables: {
file: null,
meta: {
website: website ?? "",
twitter: twitter ?? "",
telegram: telegram ?? "",
description: description ?? "",
creator,
},
},
})
);
form.append("map", JSON.stringify({ "0": ["variables.file"] }));
const imageBuf = fs.readFileSync(imagePath);
const ext = path.extname(imagePath) || ".png";
const mime = ext === ".png" ? "image/png" : ext === ".jpg" || ext === ".jpeg" ? "image/jpeg" : "image/png";
form.append("0", imageBuf, {
filename: path.basename(imagePath) || "image.png",
contentType: mime,
});
const res = await axios.post(FLAP_UPLOAD_API, form, {
headers: form.getHeaders(),
maxBodyLength: Infinity,
maxContentLength: Infinity,
});
if (res.status !== 200) {
throw new Error(`上传失败: ${res.status} ${res.statusText}`);
}
const cid = res.data?.data?.create;
if (!cid || typeof cid !== "string") {
throw new Error("响应中无 create CID: " + JSON.stringify(res.data));
}
return cid;
}
async function main() {
const args = process.argv.slice(2);
const imagePath = args[0];
if (!imagePath) {
console.error("用法: node upload-token-meta.js <图片路径> [简介] [官网] [twitter] [telegram]");
console.error("示例: node upload-token-meta.js ./logo.png \"代币简介\" \"https://example.com\"");
process.exit(1);
}
const [description, website, twitter, telegram] = args.slice(1);
const cid = await uploadTokenMeta({
imagePath,
description: description ?? "",
website: website || null,
twitter: twitter || null,
telegram: telegram || null,
});
console.log(cid);
}
main().catch((err) => {
console.error(err.message || err);
process.exit(1);
});
```
### scripts/mm-generate-workers.js
```javascript
#!/usr/bin/env node
/**
* 由 Agent 自动生成做市用 worker 密钥,无需用户提供。
* 生成 N 个随机私钥及对应地址,按生成时间命名导出到本地文件,并输出地址列表(供 setAllowedCallers 使用)。
*
* 用法:node scripts/mm-generate-workers.js [N] 或 WORKER_COUNT=20 node scripts/mm-generate-workers.js
* 默认 N=20。文件名:mm-workers-<YYYYMMDD>-<HHmmss>.json(按生成时间命名)。
* 输出:--json 时 stdout 输出 { "addresses": [...], "file": "<绝对路径>" };否则每行一个地址,文件路径写 stderr。
*/
import { generatePrivateKey } from "viem/accounts";
import { privateKeyToAccount } from "viem/accounts";
import fs from "fs";
import path from "path";
const argv = process.argv.slice(2).filter((a) => a !== "--json");
const N = Math.min(Math.max(parseInt(process.env.WORKER_COUNT || argv[0] || "20", 10) || 20, 2), 20);
const asJson = process.argv.includes("--json");
const outDir = process.cwd();
const now = new Date();
const timeTag =
now.getFullYear() +
String(now.getMonth() + 1).padStart(2, "0") +
String(now.getDate()).padStart(2, "0") +
"-" +
String(now.getHours()).padStart(2, "0") +
String(now.getMinutes()).padStart(2, "0") +
String(now.getSeconds()).padStart(2, "0");
const fileName = `mm-workers-${timeTag}.json`;
const outPath = path.join(outDir, fileName);
const privateKeys = [];
const addresses = [];
for (let i = 0; i < N; i++) {
const pk = generatePrivateKey();
privateKeys.push(pk);
addresses.push(privateKeyToAccount(pk).address);
}
const data = { privateKeys, addresses, generatedAt: now.toISOString() };
fs.writeFileSync(outPath, JSON.stringify(data, null, 2), "utf8");
const absPath = path.resolve(outPath);
process.stderr.write(`已生成 ${N} 个 worker 并写入 ${absPath}\n`);
if (asJson) {
process.stdout.write(JSON.stringify({ addresses, file: absPath }));
} else {
addresses.forEach((a) => process.stdout.write(a + "\n"));
}
```
---
## Skill Companion Files
> Additional files collected from the skill directory layout.
### README.md
```markdown
# Flap Skills(蝴蝶技能)
基于 [BNB Chain MCP](https://docs.bnbchain.org/showcase/mcp/skills/) 的 AI 技能:**创建 V5 代币**(0 税或税收,四档分配)、**USDT 买入/卖出**(按数量或按比例)、**做市刷量**(每轮 5 买 5 卖,启动销毁 5 万枚,无 USDT 时卖回 funder 后继续,日志北京时间)。代币迁移后支持 PancakeSwap V2/V3。
**技能合约(BSC)**:`0x482970490d06fc3a480bfd0e9e58141667cffedc`
**技能版本**:1.8.0
---
## 依赖
本技能依赖 **BNB Chain MCP**。使用前请先完成:
- **连接 BNB Chain MCP**:运行 `npx @bnb-chain/mcp@latest`
- **安装 BNB Chain 官方技能**(可选):`npx skills add bnb-chain/bnbchain-skills`
详细说明见:[BNB Chain Skills 文档](https://docs.bnbchain.org/showcase/mcp/skills/)
---
## 安装
### Cursor / Claude
在当前项目中安装(仅当前项目可用):
```bash
npx skills add flap-builder/flap-skills
```
全局安装(所有项目可用):
```bash
npx skills add flap-builder/flap-skills -g
```
### OpenClaw
本技能已上架 [ClawHub](https://clawhub.ai)。先安装 ClawHub CLI,再安装本技能:
```bash
npm i -g clawhub
clawhub install flap-skills
```
安装后**新开一次 OpenClaw 会话**,技能才会被加载。使用方式与下方「如何使用」相同:在对话中说「**蝴蝶技能**」即可触发。OpenClaw 需已配置 [BNB Chain MCP](https://docs.bnbchain.org/showcase/mcp/skills/)(如通过官方文档页由 bot 自行拉取并配置),且环境中已设置 `PRIVATE_KEY` 才能发送链上交易。
---
## 如何使用
### 触发方式
在对话中说「**蝴蝶技能**」即可触发本技能。
### 前置条件
- 已安装并连接 [BNB Chain MCP](https://docs.bnbchain.org/showcase/mcp/skills/)
- 在 MCP 的 `env` 中已配置 `PRIVATE_KEY`,否则无法发送链上交易
### 支持的操作
| 操作 | 说明 |
|------|------|
| **创建代币** | **0 税**:只需「蝴蝶技能 创建代币 名称:… 符号:…」(可选官网、简介、图片),不需税点、税收地址。**有税**:需「税点:…% 税收地址:0x…」并可选四档分配、官网、简介、图片;Agent 按类型跑脚本生成 meta 与 salt(0 税 8888,有税 7777)并调用合约 |
| **买入** | 先授权 USDT,再按指定代币与 USDT 数量买入 |
| **卖出(按数量)** | 指定代币地址与卖出数量 |
| **卖出(按比例)** | 指定代币地址与比例(如 50%、100%) |
| **做市/刷量** | 每轮 5 买 5 卖;启动时销毁 5 万枚;无 USDT 时卖回 funder 后继续;资金归集地址必填。停止说「**停止做市刷量**」;归集说「**归集资金**」并指定代币与目标地址。见 [SKILL.md §6](SKILL.md#6-做市刷量与创建代币买卖一致用户说一句agent-自主执行) |
### 创建代币提示词示例
**0 税代币**(只说名称和符号即可):
```
蝴蝶技能 创建代币
名称:My Token
符号:MTK
```
可选补充官网、简介、代币图片。不需要说税点、税收地址。
**税收代币**(需税点与税收地址):
```
蝴蝶技能 创建代币
名称:My Token
符号:MTK
税点:3%
税收地址:0x...
官网:https://example.com
简介:这是一段代币简介
```
可选指定四档分配(四者之和 100%),例如:**营销税点:50% 持币分红税点:25% 回购销毁税点:15% LP回流税点:10%**;启用持币分红时可选「**最低持币数量:1 万**」。未指定分配时默认全部归营销。
(有官网、简介或图片时,Agent 会跑 `scripts/upload-token-meta.js` 等脚本完成 meta 与 salt。)
### 完整提示词示例(实测可直接用)
以下为实测可用的自然语言示例,复制后替换地址或参数即可使用。
**示例 1:创建 0 税代币**(只说名称、符号,可选官网与简介)
```
蝴蝶技能 创建代币
名称:0税
符号:0税
官网:https://github.com/flap-builder/flap-skills
简介:这是一个AI Agent使用蝴蝶技能创建的0税测试代币,CA:0xe139ca52ffd33d7cbb0dfeaf075f943c13937777
```
(附上代币图片即可;不写税点、税收地址。)
**示例 2:创建税收代币**(四档分配 + 持币门槛)
```
蝴蝶技能 创建代币
名称:TEST
符号:TEST
税点:10%
税收地址:0x62F5cCb8b1744A427b7511374F4eb33114217199
营销税点:80% 持币分红税点:10% 回购销毁税点:10%
最低持币数量:50000
```
(LP 回流未写即 0%,四档之和 80+10+10+0=100%。附代币图片可选。)
**示例 3:买入**
```
蝴蝶技能 用 0.01 U 买入 0x37be760e5fb95f9457137b6cb5b33b0be89a7777
```
**示例 4:卖出全部**
```
蝴蝶技能 卖出所有的 0x37be760e5fb95f9457137b6cb5b33b0be89a7777
```
其他说法:按数量「蝴蝶技能 卖出 100 个 0x…」、按比例「蝴蝶技能 卖出 50% 的 0x…」。
### 买入 / 卖出示例(简要)
- 「蝴蝶技能 用 0.01 U 买入 0x…」 / 「蝴蝶技能 用 10 USDT 买入 0x…」
- 「蝴蝶技能 卖出 100 个 0x…」
- 「蝴蝶技能 卖出 50% 的 0x…」 / 「蝴蝶技能 卖出所有的 0x…」
### 做市/刷量示例(资金归集地址必填)
```
使用蝴蝶技能对 0xe139ca52ffd33d7cbb0dfeaf075f943c13937777 进行做市刷量,随机范围:1-10U,资金归集地址:0x62F5cCb8b1744A427b7511374F4eb33114217199
```
Agent 将自动:生成 20 个 worker → **自主向每人转 0.001 BNB 作 Gas**(无需你手动转)→ **MCP 对技能合约授权 USDT、setAllowedCallers 登记 worker** → 启动做市脚本(**worker 调用合约** buyForCaller/sellForCaller 买卖)。**只有你说「停止做市刷量」或 Ctrl+C 会停止**,停止后剩余代币与 BNB 归集到上述地址。**做市过程中若某 worker Gas 不足**,Agent 会**自主**用 MCP 向该 worker 转 0.001 BNB 补 gas,**无需主人批准**。
**停止做市**:说「**停止做市刷量**」或「蝴蝶技能 停止做市刷量」,Agent 会停止做市脚本(本轮结束后执行归集);或在运行 mm-bot 的终端按 **Ctrl+C**。
**归集资金**:说「**归集资金**」或「蝴蝶技能 归集资金」,并说明代币地址与归集目标地址(或指定上次做市用的 worker 文件),Agent 会执行 `mm-collect.js` 将各 worker 的剩余代币与 BNB 归集到目标地址。例:「蝴蝶技能 归集资金,代币 0xe139…37777,归集到 0x62F5…199,worker 文件 mm-workers-20260305-074301.json」。
更多合约接口与 ABI 见仓库内 [SKILL.md](./SKILL.md) 与 [references/contract-abi.md](./references/contract-abi.md)。
---
## 捐款
如本技能对你有帮助,欢迎捐赠:
**捐款地址**:`0x62F5cCb8b1744A427b7511374F4eb33114217199`
**CA**:`0xe139ca52ffd33d7cbb0dfeaf075f943c13937777`
```
### _meta.json
```json
{
"owner": "flap-builder",
"slug": "flap-skills",
"displayName": "Flap Skills",
"latest": {
"version": "1.8.0",
"publishedAt": 1772710724001,
"commit": "https://github.com/openclaw/skills/commit/a3cbaeb6b7ff982ae779eb68438dc0138a00a586"
},
"history": [
{
"version": "1.6.0",
"publishedAt": 1772655673586,
"commit": "https://github.com/openclaw/skills/commit/ffbdb57e5207b9ed0406d1b8a86987658c5022b2"
}
]
}
```
### scripts/mm-collect.js
```javascript
#!/usr/bin/env node
/**
* 做市结束后资金归集:将各 worker 地址上的代币与 BNB 归集到指定地址。
* 环境变量:TARGET_ADDRESS(归集目标), TOKEN_CA(要归集的代币), PRIVATE_KEYS_FILE(worker 密钥文件)
*/
import { createPublicClient, createWalletClient, http, parseAbi, getAddress, formatEther, parseEther } from "viem";
import fs from "fs";
import path from "path";
import { privateKeyToAccount } from "viem/accounts";
import { bsc } from "viem/chains";
const ERC20_ABI = parseAbi([
"function balanceOf(address account) external view returns (uint256)",
"function transfer(address to, uint256 amount) external returns (bool)",
]);
function env(key, def) {
const v = process.env[key];
return v !== undefined && v !== "" ? v : def;
}
function loadWorkerKeys() {
const filePath = env("PRIVATE_KEYS_FILE", "");
if (!filePath) return [];
const abs = path.isAbsolute(filePath) ? filePath : path.join(process.cwd(), filePath);
if (!fs.existsSync(abs)) return [];
const data = JSON.parse(fs.readFileSync(abs, "utf8"));
const keys = Array.isArray(data.privateKeys) ? data.privateKeys : [];
return keys.filter((k) => typeof k === "string" && k.length > 0);
}
async function main() {
const targetRaw = env("TARGET_ADDRESS", process.argv[2]);
const tokenCa = env("TOKEN_CA", process.argv[3]);
if (!targetRaw || !tokenCa) {
console.error("用法: TARGET_ADDRESS=0x... TOKEN_CA=0x... PRIVATE_KEYS_FILE=mm-workers-xxx.json node scripts/mm-collect.js");
console.error(" 或: node scripts/mm-collect.js <TARGET_ADDRESS> <TOKEN_CA>");
process.exit(1);
}
const target = getAddress(targetRaw);
const token = getAddress(tokenCa);
const rpcUrl = env("RPC_URL", "https://bsc-dataseed.binance.org");
const workerKeys = loadWorkerKeys();
if (workerKeys.length === 0) {
console.error("请设置 PRIVATE_KEYS_FILE 指向 worker 密钥文件(如 mm-workers-*.json)");
process.exit(1);
}
const transport = http(rpcUrl);
const publicClient = createPublicClient({ chain: bsc, transport });
const tokenContract = { address: token, abi: ERC20_ABI };
const gasReserve = parseEther("0.0001");
console.log("归集目标:", target);
console.log("代币:", token);
console.log("Worker 数:", workerKeys.length);
for (let i = 0; i < workerKeys.length; i++) {
const account = privateKeyToAccount(workerKeys[i]);
const walletClient = createWalletClient({ account, chain: bsc, transport });
const addr = account.address;
const [tokenBalance, bnbBalance] = await Promise.all([
publicClient.readContract({ ...tokenContract, functionName: "balanceOf", args: [addr] }),
publicClient.getBalance({ address: addr }),
]);
if (tokenBalance > 0n) {
try {
const hash = await walletClient.writeContract({
...tokenContract,
functionName: "transfer",
args: [target, tokenBalance],
account,
});
await publicClient.waitForTransactionReceipt({ hash });
console.log(` [${i + 1}/${workerKeys.length}] ${addr.slice(0, 10)}… 代币 ${tokenBalance} → 已转`);
} catch (e) {
console.error(` [${i + 1}/${workerKeys.length}] ${addr.slice(0, 10)}… 代币转账失败:`, e.message || e);
}
}
const bnbLeft = await publicClient.getBalance({ address: addr });
if (bnbLeft > gasReserve) {
const sendAmount = bnbLeft - gasReserve;
try {
const hash = await walletClient.sendTransaction({
to: target,
value: sendAmount,
account,
});
await publicClient.waitForTransactionReceipt({ hash });
console.log(` [${i + 1}/${workerKeys.length}] ${addr.slice(0, 10)}… BNB ${formatEther(sendAmount)} → 已转`);
} catch (e) {
console.error(` [${i + 1}/${workerKeys.length}] ${addr.slice(0, 10)}… BNB 转账失败:`, e.message || e);
}
}
}
console.log("归集完成。");
}
main().catch((e) => {
console.error(e);
process.exit(1);
});
```
### scripts/mm-worker-addresses.js
```javascript
#!/usr/bin/env node
/**
* 从环境变量 PRIVATE_KEYS_MM(逗号分隔的私钥)输出对应地址列表,供 Agent 调用 setAllowedCallers 或配置 mm-bot 使用。
* 用法:PRIVATE_KEYS_MM=0xkey1,0xkey2 node scripts/mm-worker-addresses.js
* 输出:每行一个地址,或 --json 输出 JSON 数组。
*/
import { privateKeyToAccount } from "viem/accounts";
const raw = process.env.PRIVATE_KEYS_MM || "";
const keys = raw
.split(",")
.map((s) => s.trim())
.filter(Boolean);
if (keys.length === 0) {
process.stderr.write("请设置环境变量 PRIVATE_KEYS_MM(逗号分隔的做市 worker 私钥,2~20 个)\n");
process.exit(1);
}
const addresses = keys.map((pk) => privateKeyToAccount(pk).address);
if (process.argv.includes("--json")) {
process.stdout.write(JSON.stringify(addresses));
} else {
addresses.forEach((a) => process.stdout.write(a + "\n"));
}
```