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.eCLOB 余额
0xAf63...EOA254.530
0xe39C...Gnosis Safe087.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 Unauthorized
  • 所有 CLOB API 请求需要通过 socks5h://127.0.0.1:7897

  • polymkt.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 速查


    端点方法认证说明
    /timeGET获取服务器时间(整数)
    /balance-allowanceGETL2 HMAC查询余额,需要 signature_type + asset_type=COLLATERAL 参数
    /balance-allowance/updateGETL2 HMAC刷新余额状态(不是充值
    /orderPOSTL2 HMAC挂单(sig_type=2 使用 Safe 资金)
    /orderDELETEL2 HMAC撤单
    /data/ordersGETL2 HMAC获取开放订单
    /bookGET获取盘口

    重要注意事项


    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...(正确地址)