GaaPay 接入文档
GaaPay 是面向站群的虚拟币收款基础设施。你创建一个订单,用户在我们的收银台完成链上转账,我们负责监听、确认、入账、分账与对账,并通过签名 Webhook 通知你发货。
5 分钟接入
- 控制台 → 应用与密钥 → 创建应用,拿到
AppID与sk_test_…密钥(仅显示一次)。 - 服务端调用
POST /api/v1/orders创建订单,拿到checkout_url。 - 把用户重定向到
checkout_url,或把链接直接发给用户。 - 接收 Webhook
payment.succeeded,验签通过后再发货。
# 1) 下单(签名见下一节)
curl -X POST https://api.gaapay.com/api/v1/orders \
-H "content-type: application/json" \
-H "x-gaapay-app: app_xxx" \
-H "x-gaapay-timestamp: 1792596770" \
-H "x-gaapay-nonce: 3f1c9a2e" \
-H "x-gaapay-signature: 9d1f…" \
-d '{"amount":"19.90","merchant_order_ref":"order_20261021_001","description":"至尊月卡 · 30 天"}'
# 2) 响应
{
"object": "order",
"id": "ord_01m5f…",
"short_code": "GP-20261021-8F3A2C",
"status": "QUOTED",
"amount": 19.9,
"checkout_url": "https://checkout.gaapay.com/pay/VCCC…",
"expires_at": "2026-10-21T14:46:56.632Z"
}
MODE=sandbox 时,收银台会出现「沙箱控制台」,可一键触发正常支付、少付、多付、逾期、错链、错币、reorg、风控冻结八类场景,全部走与生产相同的入账管线。核心概念
理解三个模型,你就能理解整套系统为什么这样设计。
- Order(订单):商户视角的一笔交易,只有金额、商品、回调,不含任何链与币。
- PaymentIntent(支付意图):「这笔钱要收 19.90 USD」。用户可能先选 Base 失败再换 Tron,这是同一个意图的两次尝试。
- PaymentAttempt(支付尝试):一次具体的渠道执行,持有链、资产、收款地址、报价快照与确认要求。换一种支付方式 = 新建一个 Attempt,旧 Attempt 的地址继续监听 72 小时。
这样设计的好处:新增链或渠道不会改动订单与账本;用户切换支付方式不会产生两笔订单,对账不会混乱。
鉴权与签名
所有商户 API 请求都必须携带四个头。签名原文按行拼接,顺序不可变:
payload = timestamp + "\n" + nonce + "\n" + METHOD + "\n" + path + "\n" + raw_body
signature = hex( HMAC_SHA256(app_secret, payload) )
timestamp:Unix 秒。偏差超过 5 分钟拒绝。nonce:每次请求唯一。服务端滑动窗口去重,重复即拒绝(防重放)。path:包含查询串的路径,例如/api/v1/orders?limit=20。raw_body:原始请求体字符串(GET 为空串)。不要重新序列化后再签名。
// Node.js
import { createHmac, randomUUID } from 'node:crypto';
function signedFetch(path, body) {
const ts = Math.floor(Date.now() / 1000);
const nonce = randomUUID();
const raw = body ? JSON.stringify(body) : '';
const sig = createHmac('sha256', process.env.GAAPAY_SECRET)
.update([ts, nonce, body ? 'POST' : 'GET', path, raw].join('\n'))
.digest('hex');
return fetch(`https://api.gaapay.com${path}`, {
method: body ? 'POST' : 'GET',
headers: {
'content-type': 'application/json',
'x-gaapay-app': process.env.GAAPAY_APP,
'x-gaapay-timestamp': String(ts),
'x-gaapay-nonce': nonce,
'x-gaapay-signature': sig,
},
body: raw || undefined,
});
}
# Python
import hmac, hashlib, time, uuid, json
def sign(method, path, body: str, secret: str):
ts, nonce = str(int(time.time())), str(uuid.uuid4())
payload = f"{ts}\n{nonce}\n{method}\n{path}\n{body}"
sig = hmac.new(secret.encode(), payload.encode(), hashlib.sha256).hexdigest()
return {"x-gaapay-timestamp": ts, "x-gaapay-nonce": nonce, "x-gaapay-signature": sig}
<?php
$ts = time(); $nonce = bin2hex(random_bytes(8)); $raw = json_encode($body);
$payload = $ts."\n".$nonce."\nPOST\n".$path."\n".$raw;
$sig = hash_hmac('sha256', $payload, $secret);
// headers: x-gaapay-app / x-gaapay-timestamp / x-gaapay-nonce / x-gaapay-signature
零代码接入(L0 / L1)
没有后端也能收款:控制台生成收款链接,打开即进入收银台。适合静态站、活动页、社群直发、客服手动开单。
创建订单
POST /api/v1/orders
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
amount | string | 是 | USD 金额,最多 6 位小数。不要用浮点数拼接。 |
merchant_order_ref | string | 是 | 你的订单号。与 AppID 联合唯一,是天然的幂等键:重复提交返回同一订单。 |
description | string | 否 | 展示给用户,如「至尊月卡 · 30 天」。 |
merchant_user_ref | string | 否 | 不透明用户标识(如 u_10231)。用于超额/逾期入账归属与日限额统计。禁止传邮箱、手机号等可识别个人信息。 |
metadata | object | 否 | 任意键值,原样回传。 |
redirect_ok / redirect_fail | string | 否 | 支付成功 / 失败后的回跳地址。 |
template_id | string | 否 | 场景模板 T1–T9,一次性覆盖十几个参数(锁价、容差、结算周期…)。 |
overrides | object | 否 | 单笔订单的参数覆盖,最高优先级。安全类参数受方向锁约束。 |
split_rule_id | string | 否 | 分账规则 ID。订单会快照当时的规则版本。 |
查询 / 取消 / 退款
| 接口 | 说明 |
|---|---|
GET /api/v1/orders/:id | 按 GaaPay 订单 ID 查询,含全部 Attempt 与报价快照。 |
GET /api/v1/orders?merchant_order_ref=… | 按你自己的订单号查询。建议在 Webhook 丢失时用它兜底。 |
POST /api/v1/orders/:id/cancel | 仅未到账的订单可取消。 |
POST /api/v1/orders/:id/refund | 退款只能退回原付款地址;指定其他地址会被拒绝。超过阈值需第二人审批。 |
GET /api/v1/ping | 验签自检 + 环境确认。 |
GET /api/v1/config | 查看当前生效配置与来源层(provenance)。 |
错误码
| code | HTTP | 含义与处置 |
|---|---|---|
INVALID_REQUEST | 400 | 参数错误。不重试,修正后重新提交。 |
INVALID_SIGNATURE | 401 | 签名不匹配。检查拼接顺序与 raw_body 是否与发送时完全一致。 |
TIMESTAMP_OUT_OF_WINDOW | 401 | 时间戳偏差 > 5 分钟。校时后重试。 |
NONCE_REPLAY | 401 | 同一 nonce 被复用。每次请求必须新生成。 |
RISK_BLOCKED | 403 | 命中风控(限额 / 速度 / 名单)。可联系支持复核。 |
AMOUNT_TOO_SMALL | 400 | 低于该商户或该链的最低金额。 |
CHAIN_DISABLED | 400 | 商户未开启该网络。 |
CHANNEL_UNAVAILABLE | 503 | 渠道暂时不可用。路由会尝试备选渠道;必要时降级到 Rail B。 |
ORDER_NOT_PAYABLE | 409 | 订单当前状态不可支付(已付、已取消等)。 |
IDEMPOTENT_INFLIGHT | 409 | 相同幂等键的请求正在处理中,稍后重试即可。 |
所有错误响应都带 request_id。提工单时提供它即可精确定位到那一次调用。
事件目录
| 事件 | 是否可发货 | 说明 |
|---|---|---|
order.created | 否 | 订单创建。 |
payment.detected | 否 | mempool / 首次检出。仅体验信号,绝不能据此发货。 |
payment.succeeded | 是 | 达到放行条件。唯一的发货依据。 |
payment.settled | — | 达到最终确认数,资金转入已确认金库。 |
payment.underpaid | 否 | 少付,携带已收与差额。地址继续监听,支持多笔补齐。 |
payment.overpaid | 是 | 多付,超额部分按配置入余额,绝不吞币。 |
payment.late | 否 | 订单过期后到账,按当前汇率折算入余额。 |
payment.reorged | — | 链上回滚,你需要撤销发货或计入坏账。 |
payment.risk_hold | 否 | 风控冻结,转人工。 |
refund.succeeded | — | 退款完成。 |
验签实现(必须做)
签名串为 timestamp + "." + body,算法 HMAC-SHA256,头信息:
x-gaapay-event: payment.succeeded
x-gaapay-event-id: evt_01m5f…
x-gaapay-timestamp: 1792596770
x-gaapay-signature: v1=9d1f…
x-gaapay-delivery: 1
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifyGaaPayWebhook({ secret, timestamp, body, signature }) {
// 防重放:时间戳偏差超过 5 分钟直接拒绝
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected = 'v1=' + createHmac('sha256', secret).update(`${timestamp}.${body}`).digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(signature || '');
return a.length === b.length && timingSafeEqual(a, b);
}
event_id 幂等消费,投递是「至少一次」;③ 必须以 payment.succeeded 为准,不要相信前端回跳参数。重试与死信
非 2xx 或 10 秒超时即视为失败,按指数退避重试:10s → 30s → 2m → 10m → 1h → 6h → 24h(共 7 次,跨度约 32 小时)。全部失败进入死信队列,控制台可手动重投。
不保证顺序。请以事件中的状态与版本为准,而不是到达顺序;重复投递携带相同 event_id。
对账 · 结算 · 分账
对账
每天 UTC 00:00 日切。两条独立口径:链上事实重新累加 vs 账本分录直接求和。差异分级:漏单/多记/金额差为 P0(自动冻结出款),悬账为 P1,状态差 P2,跨日时间差 P3 自动消解。
结算
站群内部划转全部在账本内完成,不产生任何链上交易:T+0/T+1/T+3 可配,pending → available 只是一条分录,零 Gas、即时、不会失败。只有当资金需要离开 GaaPay 体系时才上链。
分账
求值顺序:固定金额 → 固定比例 → 阶梯比例 → 剩余分配(residual)。每条规则必须且只能有一个 residual 方承接尾差;各方之和必须严格等于净额,否则拒绝执行并告警。
安全实践清单
- 密钥只存服务端,绝不进前端代码、不进 Git、不进日志;使用环境变量或 KMS。
- 生产密钥与测试密钥前缀不同(
sk_live_/sk_test_),肉眼可辨。 - 开启 IP 白名单;商户 API 只接受你的服务器调用,浏览器永不直连。
- 发货逻辑必须在验签之后;金额与订单号必须与本地订单交叉校验。
- 建议同时轮询查单接口作为 Webhook 的兜底。
- 退款只退原地址;不要接受用户指定的新地址(超额支付诈骗的标准手法)。
服务条款(摘要)
GaaPay 提供的是软件与监控服务:地址派生、链上监听、确认、记账、通知、对账与报表。GaaPay 不保管、不代付、不划转商户资金;自托管模式下资金直达商户登记的收款地址,私钥由商户自行持有并承担其业务的合规责任。
商户对其业务的合法性、商品交付、税务与用户协议独立负责。加密资产转账不可撤销,商户应在发货前完成自身风控判断。
本段为摘要。具有约束力的完整文本见 《服务条款》,两者不一致时以完整文本为准。
隐私政策(摘要)
我们只采集完成支付所必需的数据:商户标识、订单号、金额、币种、链、地址、交易哈希、时间戳与状态。我们不采集玩家姓名、邮箱、手机号、证件或支付卡信息,也不做设备指纹画像。数据库中不含可识别个人身份的信息集合。
完整的数据类别、处理基础、保存期限与跨境传输说明见 《隐私政策》。
退款政策(摘要)
加密支付无拒付(chargeback),退款需由商户主动发起并承担链上 Gas。退款只退回原付款地址;若原地址不可用,资金退回商户待结算余额,由商户自行与其用户协商。退款审批遵循发起人 ≠ 审批人的四眼原则。
少付/多付/错链的自动化分流规则与争议举证流程见 《退款与争议处理政策》。