PMWallets
EN

API 文档

WebSocket 与 Webhook 实时成交推送,以及付费导出交易历史 —— 接口、消息格式,以及每个字段的含义。

这里卖两样东西,买法不一样。实时成交是订阅:订阅一个实体,它的交易发生时就推给你, 按小时计费。交易历史是一次性购买:自己挑一段日期,付一次钱,下载一个归你的文件。

三章:价格接口错误码。只想接上去开跑就直接跳到接口。

价格

实时推送:按小时

每个实体每小时 $0.05(约 $36.00/月), 从账户余额扣。订阅时先扣第一个小时,之后每过一小时扣一次;余额不够就暂停推送,不会欠账。充值本身不会把它重新打开 —— 要显式调恢复接口,而且是从当前链头恢复, 暂停期间的空档不补。取消立即停止计费,当前这个已经付掉的小时不退。

交易历史:一次性

按天收,区间越长日费率越低。分档是边际的:90 天的区间,前 30 天按 $0.17/天算,中间 30 天 $0.14,最后 30 天 $0.11,合计 $12.60 —— 后面的档位不会把前面的天数重新定价。

天数每天
1–30$0.17
31–60$0.14
61–90$0.11
91–180$0.07
181–365$0.04
366+$0.02

前 90 天最贵:近期活动才说明一个钱包现在行不行。

一次买多个实体,整单打折:

实体数折扣
1
25%
310%
420%
530%
6+40%

最少 30 天,单笔订单最多 25 个实体。没有订阅要续 —— 数据已经在你手里了。

余额与货币

同一个账户余额,先充后用。计价和扣款一律是美元,priceCents 就是美分。人民币按固定汇率 6.5 折算入账, 只是付款方式,不是第二套价目表 —— 报价里的 price.cny 仅供显示。

接口

都在 https://api.pmwallets.com 下,JSON 进 JSON 出,按实际会写的顺序排: 拿 key → 订阅 → 收推送 → 补断档,外加单独一条导出线。

鉴权:一把 API key

每个接口都用同一把 API key。Authorization: Bearer x-api-key 两个头都认(WebSocket 握手设不了前者)。

shell
# HTTP
curl https://api.pmwallets.com/v1/account/fills?limit=100 -H "Authorization: Bearer $PMW_KEY"

# WebSocket — the handshake cannot set Authorization, so the key goes in its own header
const ws = new WebSocket("wss://api.pmwallets.com/v1/ws", { headers: { "x-api-key": process.env.PMW_KEY } });

API key 页创建和删除。key 只在创建响应里出现一次 —— 我们只存哈希, 丢了只能换一把。最多 5 把,够不停机轮换,删除立即生效。

不需要 Origin 头;请求里一旦出现 key 就只认它,无效即 401。 唯一不用 key 的是 Webhook 接收端 —— 那是我们推给你,你用注册时拿到的密钥验签。

查询榜单:谁在赚钱

GET/v1/leaderboardno auth

不带 key 也能调,拿到的是 20 行的样本;带上一把 key、且账号有生效订阅, 上限提到 200 行一页、offset 最深 100,000、每天 20,000 行。period 选统计窗口(30d / 7d,默认 30d),sort + dir 排序,limit / offset 翻页。

行里给的是 handle,不是地址 —— 地址是这个产品卖的东西,谁拿到都能去 Polymarket 上看同一个钱包。每行自带 revealCents(这一个地址多少钱, 按它自己的档位),买下来见下一节。

shell
# the board itself: who is making money, and on what. No key needed for the
# free sample; a key on an account with an active subscription raises the caps.
curl -s "https://api.pmwallets.com/v1/leaderboard?sort=realizedPnl&dir=desc&limit=50" \
  -H "Authorization: Bearer $PMW_KEY"

# ...and the same endpoint filtered. Every filter is a floor or an exact match;
# they combine with AND, and nothing here changes what a row CONTAINS.
curl -s "https://api.pmwallets.com/v1/leaderboard?\
minPnl=5000&\
minWinLo=0.55&\
minEligible=30&\
style=maker&\
category=sports&\
status=active&\
maxWallets=1&\
sort=winLo&dir=desc&limit=50" -H "Authorization: Bearer $PMW_KEY"

可用的筛选条件,全部是「下限」或「精确匹配」,互相之间是 AND:

minPnlnumber已实现盈亏下限(USDC)。可以为负,用来找亏的。
minRoinumberROI 下限,按买入额算。0.2 = 20%。
minWinLonumber 0–1胜率的 Wilson 95% 下界,不是胜率本身 —— 10 胜 0 负的下界只有 0.69。这是唯一一个把样本量算进去的筛子。
minEligibleint已结算市场数下限,默认 5。样本量的地板,调低会放进一堆只交易过两三次的钱包。
minFillsint成交笔数下限(窗口内)。
minVolumenumber买入总额下限(USDC)。
maxWalletsint实体里最多几个地址。填 1 = 只要单地址实体,跟单时不用对账一个钱包簇。
stylemaker | taker | two-sided按挂单/吃单占比划分。maker 的打法通常复刻不了。
statusactive | quiet | dormant按最后一笔成交的新旧。dormant 是 7 天没动过的。
category枚举主攻的市场类别:crypto、crypto-updown、sports、politics、geopolitics、economy、culture、esports、science-tech、other、unknown。按该类别占其成交额的比重归类。

排序键:realizedPnlroiOnBuyswinLonFillsfillsPerDaygrossBuyUsdceligibleconditionsunrealizedPnllastFill。 参数写错会 400 并说明期望值,不会悄悄忽略。

json
// GET /v1/leaderboard  →  200
{
  "periodId": "30d",
  "tier": "subscriber",
  "pricing": {
    "revealCents": 100,              // the cheapest band; what a row costs is on the row
    "subscriptionCentsPerHour": 5,
    "bands": [                       // from = the pnlExTop the band starts at; null = no floor
      { "band": 0, "from": null, "cents": 0 },
      { "band": 1, "from": 0.01, "cents": 100 },
      { "band": 5, "from": 5000, "cents": 500 }
    ]
  },
  "limits": { "maxRows": 200, "maxOffset": 100000, "capped": false,
              "dailyRows": 20000, "usedToday": 1250 },
  "rows": [
    {
      "entityId": "7KQ2MF9X4B1C",    // a HANDLE until you buy the address; then the address itself
      "label": "0x4b96…984e",        // masked until then
      "revealed": false,
      "revealCents": 300,            // what THIS address costs, from its own band
      "subscribed": false,
      "realizedPnl": 48213.5, "unrealizedPnl": 1204.0, "roiOnBuys": 0.184,
      "wins": 312, "losses": 89, "eligible": 401,
      "wr": 0.778, "lo": 0.735, "hi": 0.815,   // Wilson 95% bounds on the win rate
      "pnlExTop": 41902.1,           // profit with its single best market removed — the price band
      "style": "maker", "makerShare": 0.91,
      "wallets": 2, "nFills": 18422,
      "categories": [{ "category": "sports", "share": 0.62 }],
      "status": "active",
      "activity": { "lastFillTs": "2026-09-24 01:05:47", "fills24h": 412, "fills7d": 2980 },
      "dataSource": "live", "dataPeriodId": "30d"
    }
  ]
}

买下一个地址

POST/v1/account/revealsAPI key

按钱包质量分 6 档,价格取决于 pnlExTop —— 已实现盈亏扣掉它最赚的那一个市场之后还剩多少。一场翻身仗赚出来的钱包和赢了五十次的钱包, 不该卖同一个价。band 0 是免费:扣掉最赚的一场之后没赚到钱的,我们不收费。 每行的 revealCents 就是它的价格,pricing.bands 给出完整的档位表。

shell
# buy the address behind a handle. maxPriceCents is REQUIRED and is a
# ceiling: it is the price you were shown, and the charge can never exceed it.
curl -X POST https://api.pmwallets.com/v1/account/reveals \
  -H "Authorization: Bearer $PMW_KEY" -H "content-type: application/json" \
  -d '{"entityId":"7KQ2MF9X4B1C","maxPriceCents":300}'

# what you already own, newest purchase first
curl -s https://api.pmwallets.com/v1/account/reveals -H "Authorization: Bearer $PMW_KEY"
json
// POST /v1/account/reveals  →  200
{
  "entityId": "0x4b96e2d59f0dabde95ae6f55f2d2d6d345ba984e",   // yours now
  "label": "0x4b96e2d59f0dabde95ae6f55f2d2d6d345ba984e",
  "priceCents": 300,
  "alreadyOwned": false            // true = you had it; nothing was charged
}

// 409 — the band moved between the page and the click. Nothing was charged.
{ "statusCode": 409, "error": "price_changed", "priceCents": 500, "agreedCents": 300 }

// 402 — not enough balance. Nothing was charged.
{ "statusCode": 402, "error": "insufficient_balance", "priceCents": 300, "balanceCents": 120 }

// GET /v1/account/reveals  →  200
[
  { "entityId": "0x4b96…984e", "priceCents": 300, "entitled": true,
    "createdAt": "2026-09-20T10:23:00.000Z", "paidAt": "2026-09-20T10:23:00.000Z" },
  // a FREE unlock whose wallet has since moved into a paid band: the address is masked again
  { "entityId": "9XQ4MF7K2B1C", "label": "0x8a1f…22ce", "priceCents": 0, "entitled": false,
    "revealCents": 200, "createdAt": "2026-09-18T08:00:00.000Z", "paidAt": null }
]
entityIdhandle必须是榜单给的 12 位 handle。裸地址会被 400 拒 —— 否则这个接口就成了「拿一串地址来问哪些在榜上」的确认器,而那正是要卖的信息。已经买过的地址例外。
maxPriceCentsint ≥ 0必填,且是上限:页面上显示给你的价格。档位每隔几小时随新的统计重算,涨了会 409 并告诉你新价,绝不会按新价直接扣。0 也是合法上限(免费档),不是「没设」。

付费买下的是永久的 —— 一个事实不会被收回,以后再看不再收费,订阅到期也不影响。 ⚠ 免费拿的只在这个钱包仍然免费时有效:如果它之后赚到钱进了收费档,再看要按新档付费 (点「免费查看」时就写明了)。GET /v1/account/reveals 里, 这类行会重新变回 handle,并带上 entitled: false 和新的价格。

一天能问多少个不同实体是有上限的(超了 403),这道闸防的是拿账号去把整张榜刷下来。 买过的再问不算数。

订阅一个实体

POST/v1/account/subscriptionsAPI key

entityId 两种写法都收:榜单给你的 handle(12 位,形如 7KQ2MF9X4B1C),或者你已经拿到的 0x 地址。榜单上只给 handle 和打码后的地址 —— 地址本身谁拿到都能去 Polymarket 上看同一个钱包;完整地址在你订阅了的实体页上给你,推送的成交里也带。channels wswebhook,或者两个都要。 订阅之后只推此刻之后发生的成交 —— 起点是当前链头,往前的历史走导出那条线。

shell
# the full 40-hex address the leaderboard shows — an abbreviated one is a 400"""
ENTITY=0x9d84ce0306f8551e02efef1680475fc0f1dc1344

curl -X POST https://api.pmwallets.com/v1/account/subscriptions \
  -H "Authorization: Bearer $PMW_KEY" -H 'content-type: application/json' \
  -d "{\"entityId\":\"$ENTITY\",\"channels\":[\"ws\",\"webhook\"]}"

# 409 if the entity has had no fill in 7 days — resend with "acceptInactive": true
# 402 if the balance will not cover the first hour; body carries price and balance
json
// POST /v1/account/subscriptions  →  201
{
  "id": "sub_9f2c1a7e",
  "userId": "usr_3b8d",
  "chain": 137,
  "entityId": "0x9d84ce0306f8551e02efef1680475fc0f1dc1344",
  "channels": ["ws", "webhook"],
  "status": "active",
  "fromBlock": 94146218,       // delivery starts AFTER this block
  "createdAt": "2026-09-21T02:14:07.611Z",
  "canceledAt": null
}

// GET /v1/account/subscriptions  →  200   (active + paused, newest first)
[ { "id": "sub_9f2c1a7e", "entityId": "0x9d84…", "status": "active", "fromBlock": 94146218, … } ]

// 402 — the balance will not cover the first hour
{ "statusCode": 402, "message": "insufficient balance for the first hour",
  "priceCentsPerHour": 5, "balanceCents": 0 }

// 409 — the entity has not traded recently; resend with acceptInactive
// (no figures about the entity: naming an address here would otherwise describe it for free)
{ "statusCode": 409, "message": "entity is dormant: no fill in the last 7 days",
  "status": "dormant", "hint": "resend with acceptInactive: true to subscribe anyway" }

状态 active / paused / canceled,余额耗尽自动转 paused。 最多 200 个生效订阅。实体 7 天没成交会被 409 挡下, 带 acceptInactive: true 重发即可 —— 这道闸防的是给一个已经收摊的 钱包按小时付钱。

列出订阅

GET/v1/account/subscriptionsAPI key

activepaused,按创建时间倒序。已取消的不在里面,所以"某个实体还推不推"看它在不在这个列表里就够了。 字段见上面的示例。

取消订阅

DELETE/v1/account/subscriptions/:idAPI key

计费到此为止,已预付的那小时不退。取消是终态,同一实体可以重新订, 但那是一条新订阅、新的 fromBlock,中间那段不补。

余额不足暂停后恢复

POST/v1/account/subscriptions/:id/resumeAPI key

订阅不能手动暂停 —— 不想要了就取消。唯一的暂停是系统做的:到了续费的整点余额不够扣一小时, 订阅变成 paused(pausedReason: "insufficient_balance"), 推送随即停止,不再扣费。充值后用它。重新扣一个小时,从当前链头开始 —— 暂停期间的成交不补推。已 canceled 的恢复不了(409),直接重新订。

shell
# what is still being pushed — active and paused only, newest first
curl -s https://api.pmwallets.com/v1/account/subscriptions -H "Authorization: Bearer $PMW_KEY"

# stop one. Billing stops here; the hour already paid for is not refunded
curl -s -X DELETE https://api.pmwallets.com/v1/account/subscriptions/$SUB_ID \
  -H "Authorization: Bearer $PMW_KEY"

# restart one that ran out of balance. Charges another hour and resumes from the
# CURRENT head — the gap while it was paused is not backfilled
curl -s -X POST https://api.pmwallets.com/v1/account/subscriptions/$SUB_ID/resume \
  -H "Authorization: Bearer $PMW_KEY"

从出块到发出通常在一秒以内;延迟页公布的是实测的 p50 和 p95, 不是一句承诺。

WebSocket

GETwss://api.pmwallets.com/v1/wsAPI key

这条连接上没有什么要订阅的。它推送的就是你账户已经订阅的那些实体的成交,你发什么过来都会被忽略。 这是低延迟通道,也是跟单机器人通常会连的那一条:一条长连接,不用每笔事件重做 DNS 和 TLS 握手, 也不要求你自己有公网 HTTPS 地址。本站的实时看板用的是同一条流。

node
import WebSocket from "ws";

const ws = new WebSocket("wss://api.pmwallets.com/v1/ws", {
  headers: {
    "x-api-key": process.env.PMW_KEY,       // a handshake cannot set Authorization
  },
});

代价是这条 socket 本身不重试。帧是尽力投递:交给打开的 socket 就算发出, 没有 socket 就丢弃,两者都不会重发。但丢掉的不等于没了 —— 存好游标,用 GET /v1/account/fills 把空档拉回来,下一节讲的就是这件事。 如果你更希望成交是被推过来而不是自己去拉,那就同时接上 Webhook;两种做法都按 eventId 去重。

每个账户 1 条连接。再开一条会接管:旧连接以 1000 正常关闭, 最新的那条成为实时连接 —— 一个忘了关的标签页不会占住你唯一的流。

怎么知道自己漏了,以及怎么补

每一帧都带 sessionseq。同一个 session 内 序号是连续的,所以序号跳了就说明我们丢了帧 —— 消费端跟不上时我们会故意丢帧, 连接还开着,除了这个序号没有别的东西会告诉你。session 变了说明你重连过。两种情况你都知道自己漏了。

js
// Persist these across reconnects — comparing against a session you just overwrote
// can never detect anything.
let session = load("session"), seq = load("seq") ?? 0;
let lastBlock = load("block") ?? 0, lastLogIndex = load("logIndex") ?? 0;

// One message at a time: a replay is awaited, and a second frame arriving meanwhile
// would advance the cursor past the gap being repaired. Note the catch — chaining
// onto a rejected promise skips every later .then(), so a single failed replay would
// leave the socket open and silently processing nothing at all.
let queue = Promise.resolve();
ws.onmessage = (e) => {
  queue = queue
    .then(() => handleFrame(JSON.parse(e.data)))
    .catch((err) => { report(err); ws.close(); });   // reconnect and replay rather than go quiet
};

async function handleFrame(m) {
  if (m.type === "hello") {
    // A NEW session means frames were missed while the socket was down. Replay BEFORE
    // adopting it — overwriting session here is what silently swallows the outage.
    // a failing replay must not be swallowed: leave the cursor where it is and throw, so the
    // handler above closes the socket and the next connection tries again from the same place
    if (session !== null && m.session !== session) await replayFrom(lastBlock, lastLogIndex);
    session = m.session; seq = m.seq; save();
    return;
  }
  if (m.type !== "fill") return;

  if (m.session !== session || m.seq !== seq + 1) {   // dropped frame, or a session we never saw
    await replayFrom(lastBlock, lastLogIndex);
    session = m.session;
  }
  seq = m.seq;

  if (!seen.has(m.data.eventId)) { seen.add(m.data.eventId); handle(m.data); }
  lastBlock = m.data.block; lastLogIndex = m.data.logIndex;
  save();
}

然后把空档拉回来。GET /v1/account/fills 按游标返回你订阅的全部实体之后的 成交,时间正序,keyset 分页 —— 和推送是同样的行、同样的 eventId,所以按它去重之后,多拉一些也不会出问题。

js
// walk forward until the page is short; every row carries the same eventId as the stream
async function replayFrom(block, logIndex) {
  for (;;) {
    const r = await fetch(
      `https://api.pmwallets.com/v1/account/fills?sinceBlock=${block}&sinceLogIndex=${logIndex}&limit=500`,
      { headers: { "x-api-key": PMW_KEY } },
    );
    if (!r.ok) throw new Error(`replay failed: HTTP ${r.status}`);   // do NOT advance the cursor
    const page = await r.json();

    for (const row of page.rows) if (!seen.has(row.eventId)) { seen.add(row.eventId); handle(row); }
    if (!page.next) return;
    ({ sinceBlock: block, sinceLogIndex: logIndex } = page.next);
  }
}

Webhook

订阅页登记一个 HTTPS 地址,我们把每笔成交 POST 过去。签名密钥只在登记那一刻显示一次,之后没有任何办法再读到,当场就要存好。每个账户 1 个地址,要换地方投递就先删掉旧的。要发给两个系统, 请在你自己那边分发 —— 那里你才看得见是哪一个坏了。

登记和删除只能在站内做,API key 不行:webhook 是一个投递目的地, 而能用一把 key 换掉投递目的地,就等于能把你的成交流永久接管到别处 —— 即使你后来删掉了那把 key, 那个 webhook 依然活着。

每个请求带 x-pmw-signature:用你的密钥对原始 body 做 HMAC-SHA256,十六进制。校验一定要对着原始字节做 —— 解析成 JSON 再序列化回去字节就变了,那之后每个签名都会对不上。

node
import { createHmac, timingSafeEqual } from "node:crypto";

app.post("/hook", express.raw({ type: "application/json" }), (req, res) => {
  const sent = Buffer.from(req.header("x-pmw-signature") ?? "", "hex");
  const mine = createHmac("sha256", process.env.PMW_SECRET).update(req.body).digest();

  // compare the RAW body, before any JSON parsing: re-serialising changes the bytes
  if (sent.length !== mine.length || !timingSafeEqual(sent, mine)) return res.sendStatus(401);

  res.sendStatus(200);          // 2xx first — we retry anything else
  void handle(JSON.parse(req.body.toString()));
});

投递是至少一次。我们等 5 秒拿 2xx,最多重试 3 次; 发送后、记录前崩溃也会重放一次。所以同一笔成交可能到两次,你的处理必须幂等 —— 按 eventId 去重,它在每次重试中都不变。

我们不跟随重定向,也只接受公网地址,所以填 localhost 或内网段的地址在注册时就会被拒。

成交事件

两个通道投递的是同一个对象,Webhook 的 body 和 WebSocket 的帧逐字节相同。

json
{
  "type": "fill",
  "data": {
    "eventId": "137:93912410:0x8f2c…a91b:0x4d0e…77c3:41",
    "chain": 137,
    "entityId": "0x9a3f…21e8",
    "wallet": "0x5b71…0cd2",
    "ts": "2026-09-16 14:02:11",
    "block": 93912410,
    "blockHash": "0x8f2c…a91b",
    "txHash": "0x4d0e…77c3",
    "logIndex": 41,
    "exchange": "pm_ctf_v2",
    "side": "BUY",
    "role": "taker",
    "tokenId": "3200000000…",
    "price": "0.570000",
    "shares": "3200000000",
    "usdc": "1824000000",
    "fee": "0"
  }
}
eventIdstring这笔成交的稳定 id:链、区块、区块哈希、交易、日志序号。去重就用它。
chainnumber137(Polygon)。写进 id 里,以后加链也不会有歧义。
entityId0x address你订阅的实体 —— 是交易者本人,不一定是签名的那个地址。
wallet0x address真正下单的地址。一个实体通常有好几个。
tsstring (UTC)区块时间,UTC。不是我们推送的时间 —— 那段差值在延迟页上。
blocknumber成交被打包进的区块。
blockHash0x hash该区块的哈希。重组后同一笔交易被重新打包,哈希不同,就是一个新事件。
txHash0x hash交易哈希。
logIndexnumber日志在区块内的位置。
exchangestring由哪个 Polymarket 交易所合约撮合的,例如 pm_ctf_v2。
side"BUY" | "SELL"从这个钱包的角度看的买卖方向。
role"maker" | "taker"这个钱包是 maker 还是 taker。只有 taker 付手续费。
tokenIdstring (uint256)结果代币(ERC-1155 id),十进制字符串 —— JS 的 number 装不下。
pricestring (decimal)每股价格(USDC),十进制字符串。
sharesstring (integer)整数,隐含 6 位小数(micro-shares)。3200000000 是 3,200 股。
usdcstring (integer)整数,隐含 6 位小数(micro-USDC)。1824000000 是 $1,824。
feestring (integer)合约实际收取的现金手续费,micro-USDC。maker 为 0。

金额是隐含 6 位小数的整数,用字符串传。它们就是合约发出来的值,我们不转成浮点 —— 一个要拿去和区块浏览器对账的文件,不该在送出的路上被四舍五入过。显示时除以 1,000,000。

发生重组时我们不撤回任何东西。同一笔交易被重新打包进另一个区块,你会收到第二个事件,blockHash 不同 —— 这就是为什么区块哈希是 eventId 的一部分,也是为什么只按交易哈希去重会悄悄丢掉一笔真实成交。

导出:先报价

POST/v1/account/exports/quoteAPI key

不扣钱、不落库,也不读你报出来的那些钱包 —— 价格只取决于日期区间和实体个数。不返回行数:一个区间上的精确行数足以认出一个钱包,而报价免费, 那就成了拿地址去试榜单 handle 的工具。真实行数在你买下的订单上。

shell
curl -X POST https://api.pmwallets.com/v1/account/exports/quote \
  -H 'content-type: application/json' \
  -H "Authorization: Bearer $PMW_KEY" \
  -d '{"entityIds":["0x9a3f…21e8"],"from":"2026-06-01","to":"2026-08-29"}'
json
{
  "window":   { "from": "2026-05-04", "to": "2026-09-19", "days": 139 },
  "from": "2026-06-01", "to": "2026-08-29", "days": 90,
  "sellable": true,
  "entities": 1,
  "orderPriceCents": 1260,
  "perEntityCents": 1260,
  "discount": 1,
  "price": { "cents": 1260, "usd": 12.6, "cny": 82 },
  "breakdown": [
    { "fromDay": 1,  "toDay": 30, "days": 30, "perDayCents": 17, "cents": 510 },
    { "fromDay": 31, "toDay": 60, "days": 30, "perDayCents": 14, "cents": 420 },
    { "fromDay": 61, "toDay": 90, "days": 30, "perDayCents": 11, "cents": 330 }
  ]
}

卖不了的区间返回 sellable: false 和一个原因 —— below_minimumno_data —— 而不是报错, 这样选择器可以把区间显示出来并解释,而不是把表单拒掉。window 是我们实际持有的跨度,你传的区间会被夹到它里面。

GET /v1/account/exports/window 返回这个跨度,以及四个快捷档 (30、60、90 天和全部)的现成报价。

导出:下单与下载

POST/v1/account/exportsAPI key

body 和报价接口一样。价格在这里按费率卡重算 —— 你那边报价显示的数字只是显示,绝不会作为扣款的输入。 接口在订单付款完成时就返回,文件由独立的 worker 生成,所以要轮询订单直到 ready

状态流转是 paidbuilding ready。生成遇到瞬时故障会退回 paid 重试; 确实生成不出来的会变成 refunded 并带上原因,余额自动退回 —— 我们不会为一个不存在的文件留着钱。

shell
# same body as the quote — the price is recomputed here, never taken from the client
curl -X POST https://api.pmwallets.com/v1/account/exports \
  -H 'content-type: application/json' -H "Authorization: Bearer $PMW_KEY" \
  -d '{"entityIds":["0x9a3f…21e8"],"from":"2026-06-01","to":"2026-08-29"}'
# → {"id":"7c1e…","priceCents":1260,"days":90,"rows":12483,"status":"paid"}

# poll until ready
curl https://api.pmwallets.com/v1/account/exports/7c1e… -H "Authorization: Bearer $PMW_KEY"
# → {"id":"7c1e…","status":"ready","rows":12483,"fileBytes":412887,…}

# then mint a link (valid 15 minutes, minted fresh each time)
curl https://api.pmwallets.com/v1/account/exports/7c1e…/download -H "Authorization: Bearer $PMW_KEY"
# → {"url":"https://…r2.cloudflarestorage.com/…","expiresInSec":900,"fileName":"pmwallets-fills-…csv.gz"}

下载链接是签名的,有效 15 分钟。每次调用都新签一个,一个都不存 —— 所以订单记录是一条购买记录,而不是给任何看到它的人的一个可用链接。要用随时再要一个; 过期的是链接,不是文件。

文件本身是 gzip CSV,按时间正序,列和实时推送完全一致 —— 所以文件和数据流可以按 eventId 一行一行对上。

csv
eventId,chain,entityId,wallet,ts,block,blockHash,txHash,logIndex,exchange,side,role,tokenId,price,shares,usdc,fee
137:93912410:0x8f2c…:0x4d0e…:41,137,0x9a3f…21e8,0x5b71…0cd2,2026-09-16 14:02:11,93912410,0x8f2c…,0x4d0e…,41,pm_ctf_v2,BUY,taker,3200000000…,0.570000,3200000000,1824000000,0

这是完整的链上账本,不是某个 API 的采样。Polymarket 自己的成交接口每个钱包最多给到 10,000 条历史成交; 这里没有这个上限,因为行是从我们自己采集的 Polygon 日志里读的,而且一个实体名下的多个地址已经合并进同一个文件。

错误码

状态码什么情况怎么处理
401key 缺失、无效或已删除换一把有效的 key
402余额不够。body 带 priceCents / balanceCents(订阅是 priceCentsPerHour)充值后重试
403这个钱包超出你当前档位能打开的范围订阅后可开榜上任意实体
409实体 7 天内没有成交acceptInactive: true 重发
409已经订过这个实体了先取消或直接用现有订阅
400not_sellable —— 区间不足 30 天,或这段没有数据拉长区间
400too_large —— 单子太大缩小区间,或把实体拆成多单
400not_ready —— 文件还在生成,body 带当前状态轮询订单直到 ready
400已有 200 个生效订阅先取消一个

常见问题 · 数字是怎么算的 · 实测推送延迟