CLOB 下单余额问题 — 解决文档
Status: ✅ RESOLVED (2026-04-30 凌晨)
前置文档: [clob-v2-migration.md](./clob-v2-migration.md) | [clob-v2-signature-debug.md](./clob-v2-signature-debug.md)
问题
v2 签名调试完成后,下单返回 HTTP 400:
{"error":"not enough balance"}
钱包有 337 USDC 链上,但 CLOB 认为余额为 0。
关键发现
1. CLOB 余额 ≠ 链上余额
Polymarket CLOB 维护独立于链上的内部记账系统。即使钱包持有大量 USDC,CLOB 也不会自动识别。用户需要将 USDC 存入 CLOB 平台(通过 polymarket.com/portfolio),该余额才会显示在 /balance-allowance 端点。
但我们的情况不同——
2. 资金在 Safe (Gnosis Safe),不在 EOA
实际资金结构:
| 地址 | 类型 | 链上 USDC.e | CLOB 余额 |
|---|---|---|---|
0xAf63... | EOA | 254.53 | 0 |
0xe39C... | Gnosis Safe | 0 | 87.15 |
Safe 是 Polymarket Proxy(Gnosis Safe 工厂创建),拥有者是 EOA。Safe 里已经有 87.15 USDC 的 CLOB 余额。问题在于代码没有使用 Safe 来下单。
3. 错误的 Safe 地址
原代码中使用的地址 0xE39Ce51148A560b8C0Ae27bAeACB23aEdd74C0b 只有 39 个 hex 字符(缺少 1 个字符),不是合法的以太坊地址。eth_abi 库拒绝编码该地址。
正确的 Safe 地址(40 个 hex 字符,EIP-55 校验和):
0xe39C4853A6e14045F192B65EfbACECf845d74C0B
4. 代理问题
本机 DNS 屏蔽所有外部域名(GFW 污染 → 198.18.x.x),需要 SOCKS5 代理。CLOBClient 没有配置代理,导致:
_get('/time') 获取服务器时间失败 → HMAC 签名回退到本地时间 → 401 Unauthorizedsocks5h://127.0.0.1:7897polymkt.lt.sopher.cool:4433 代理对 CLOB API 已失效(返回 404),直接访问 clob.polymarket.com 即可。
解决方案
修改 1: CLOBClient 内置代理 (clob_client.py)
def __init__(self, ..., proxy: dict = None):
...
# 默认通过 SOCKS5 代理访问外部网络
if proxy is not None:
self.session.proxies.update(proxy)
else:
self.session.proxies.update({
'http': 'socks5h://127.0.0.1:7897',
'https': 'socks5h://127.0.0.1:7897',
})
修改 2: 自动检测 Safe 地址 (clob_client.py)
def create_order(self, ..., funder_address: str = None):
# 如果未显式传入 funder_address,从环境变量自动读取 Safe 地址
if funder_address is None:
funder_address = os.environ.get('POLYMARKET_SAFE_ADDRESS', '').strip() or None
if funder_address:
maker = funder_address # 资金来自 Safe
sig_type = 2 # POLY_GNOSIS_SAFE
else:
maker = signer_addr # 资金来自 EOA(余额为 0)
sig_type = 0
修改 3: Web app 启动时加载 .env (app.py)
# 在 app.py 顶部手动加载 .env 到 os.environ
_env_path = os.path.join(...)
if os.path.exists(_env_path):
for line in open(_env_path):
if '=' in line:
key, val = line.split('=', 1)
if key.strip() not in os.environ:
os.environ[key.strip()] = val.strip()
修改 4: 余额显示改为 CLOB 余额 (app.py + mm_weather.html)
/api/mm/weather/balance 原来只查 Safe 的链上余额(0 USDC),改为同时查询 CLOB 余额(87.15 USDC),前端优先显示 CLOB 余额。
修改 5: quick-order 缓存失效 (app.py)
api_mm_weather_quick_order 下单成功后缺少 _invalidate_orders_cache() 调用,导致挂单列表 15 秒内不刷新。已补上。
修改 6: 撤单确认增加合约信息 (mm_weather.html)
撤单确认框原来只显示订单 ID,现在增加了方向、数量、价格、合约名称等信息。
下单签名流程(sig_type=2 POLY_GNOSIS_SAFE)
┌──────────────────────────────────────────────────────┐
│ 1. EOA 私钥签署 EIP-712 Order(signer=EOA) │
│ - maker: Safe 地址 │
│ - signer: EOA 地址 │
│ - signatureType: 2 (POLY_GNOSIS_SAFE) │
│ - timestamp: 毫秒级 (Date.now()) │
│ - verifyingContract: v2 交易所地址 │
│ - eth_account 版本: 0.12.x (标准 EIP-712 前缀) │
├──────────────────────────────────────────────────────┤
│ 2. L2 HMAC 签名 HTTP 请求头 │
│ - 签名字符串: timestamp + METHOD + path + body │
│ - 服务器时间: GET /time (避免时钟偏差) │
│ - 密钥: URL-safe base64 解码后的 32 字节 │
├──────────────────────────────────────────────────────┤
│ 3. POST /order (socks5h://127.0.0.1:7897) │
│ - 消息体: {deferExec, postOnly, order, owner, │
│ orderType} │
│ - 链上验证: Safe 合约的 EIP-1271 isValidSignature │
└──────────────────────────────────────────────────────┘
环境变量(.env)
POLYMARKET_L2_API_KEY=70be0740-...
POLYMARKET_L2_SECRET=<base64url encoded>
POLYMARKET_L2_PASSPHRASE=<passphrase>
POLYMARKET_WALLET_ADDRESS=0xAf63F116D074Ba2793CBAa83F3380F7e10dfc51d
POLYMARKET_PRIVATE_KEY=<EOA private key>
POLYMARKET_SAFE_ADDRESS=0xe39C4853A6e14045F192B65EfbACECf845d74C0B
CLOB API 速查
| 端点 | 方法 | 认证 | 说明 |
|---|---|---|---|
/time | GET | 无 | 获取服务器时间(整数) |
/balance-allowance | GET | L2 HMAC | 查询余额,需要 signature_type + asset_type=COLLATERAL 参数 |
/balance-allowance/update | GET | L2 HMAC | 刷新余额状态(不是充值) |
/order | POST | L2 HMAC | 挂单(sig_type=2 使用 Safe 资金) |
/order | DELETE | L2 HMAC | 撤单 |
/data/orders | GET | L2 HMAC | 获取开放订单 |
/book | GET | 无 | 获取盘口 |
重要注意事项
1. sig_type=1 (POLY_PROXY) 不被 CLOB 支持,必须用 sig_type=0 (EOA) 或 sig_type=2 (POLY_GNOSIS_SAFE)
2. 直接链上 deposit 调用均失败 — CLOB 出入金只能通过 polymarket.com 网站 UI
3. CLOB 余额 = 0 的账户无法下单 — 即使链上有 USDC
4. 代理必须用 socks5h(远程 DNS)不是 socks5(本地 DNS),否则 DNS 污染导致连接失败
5. eth_account 必须 0.12.x — 0.13.x 的 EIP-712 编码不兼容 Polymarket
文件修改汇总
| 文件 | 变更 |
|---|---|
src/mm/clob_client.py | __init__ 加代理支持;create_order 自动检测 POLYMARKET_SAFE_ADDRESS |
src/web/app.py | 启动加载 .env;balance 端点增加 CLOB 余额;quick-order 增加缓存失效 |
src/web/templates/mm_weather.html | 余额显示 CLOB 优先;撤单确认显示合约详情 |
.env | 已有 POLYMARKET_SAFE_ADDRESS=0xe39C...(正确地址) |