0G Pay
Docs / Partner Regrant 接口

Partner Regrant 接口

面向 Partner:通过接口把你持有的额度转赠给自己的用户 —— 你会从我们这里拿到什么、约束条件与接口契约。

面向 Partner面向运营
EN中文

开始之前:你会从我们这里拿到什么

先把你要用来签名的钱包地址发给我们。落座完成、额度发到这个钱包后,我们会发给你一份partner 接入信息包,包含下面五项 —— 有了它就能发出第一个请求。然后照着 完整示例做。

用 Google、X、Discord 或 TikTok 注册的?你的额度在一个 Privy 钱包里。 点本页右上角的 Log in 登录后,可以直接在本页签名, 私钥不会离开 Privy。只有要让你自己的后端签名时才需要私钥 —— 这时打开钱包菜单,选 Export private key。私钥显示在 Privy 自己的窗口里,我们看不到,请像保管任何私钥一样保管它。

签名钱包你提供给我们的地址,已落座为 partner。每个请求都用它签名,额度也在它名下。
API Base URLPay API 的 host。不是本站,也没有公开,所以随信息包一起给你。
source_lot_id你要从中转赠的 Lot:32 位 hex,不是 campaign id。你的钱包没有接口能列出自己的 Lot。
Lot 的额度、到期日和服务范围最多能转出多少、每行最晚能设到哪天到期、额度是否锁定在某个服务。
external_ref 前缀你的钱包地址(小写)加冒号。每一行的引用号都必须以它开头。

1. POST /v1/regrants

从你的服务端调用 partner 接入信息包里的 Pay API host。它不是本站。Pay API 不返回 CORS 头,浏览器调不了,私钥也不该放在浏览器里。没有 API key,你的签名就是你的身份。如果你在前面加了代理,签名里仍写 Pay API 实际收到的路径 /v1/regrants。
POST https://<pay-api-host>/v1/regrants
                                     # host 不是本站 —— 在你的 partner 接入信息包里
X-Pay-Timestamp: 1789000000          # unix 秒,与我们的时钟相差需在 5 分钟内
X-Pay-Signature: 0x…                 # 对下面那一行字符串做 personal_sign

{"source_lot_id": "01a0…",
 "items": [
   {"external_ref": "0xacme…:sep-airdrop:u-1",
    "account_ref": {"kind": "evm_address", "id": "0xAAA…"},
    "amount_micros": 10000000,                      # $10.00
    "expires_at": "2026-10-15T00:00:00Z"},
   {"external_ref": "0xacme…:sep-airdrop:u-2",
    "account_ref": {"kind": "evm_address", "id": "0xBBB…"},
    "amount_micros": 10000000}                      # 不填到期日 → 继承自己 Lot 的
 ]}
  • 单次最多 500 行。金额是 micro-USD 整数:$10.00 写作 10000000。
  • 收款方只能是钱包地址。没用过 Pay 的钱包会自动建账户;不接受邮箱和登录 ID。
  • 按请求顺序逐行付款。某一行要求的金额超过 Lot 剩余余额时,该行报 lot_unusable,所以把重要的付款排在前面;请逐行读取结果,不要假设第一个被拒的行之后全部失败。
  • expires_at 可选,上限是你 Lot 的到期日。填得更晚不会报错,会直接给 Lot 的到期日,所以请以响应里的值为准。
  • 每行可以带一个可选的 service 字段。不填则继承你 Lot 的;如果你的额度锁定在某个服务上,填了别的服务会被拒(scope_widened)。
  • 付款无法通过接口撤回。发送前请核对钱包名单;发错地址需要撤销时,请联系对接的 BD。

签名

pay-v2|POST|/v1/regrants|1789000000|9b74c9897bac770ffc029102a200c5de…

  pay-v2        字面常量 —— 不是你的名字,也不是版本号
  POST          HTTP 方法,大写
  /v1/regrants  路径;将来如果有 query string 也要带上
  1789000000    和 X-Pay-Timestamp 里发的是同一个 unix 秒
  9b74c989…     请求体的 sha256,小写 hex,64 个字符 ——
                签的是哈希,不是请求体本身

用 "|" 连成一行。没有换行,没有空格,末尾没有分隔符。
对这一行做 EIP-191 personal_sign,发 0x + 65 字节 r||s||v。
v 是 0/1 还是 27/28 都行,两种都能恢复出地址,你的库给什么就发什么。
哈希你真正发出去的那串字节 —— 重新序列化一遍 JSON 再哈希,
会得到不同的摘要,也就会恢复出不同的签名者。

签错了字符串,照样能恢复出一个地址,只是不是你的,结果是 403。 确信钱包已开通却收到 403 时,先逐字符比对签名串。不要发 X-Actor-Subject,这个接口会以 400 拒绝。

2. 完整示例

按信息包设置 PAY_API_URL、PARTNER_PRIVATE_KEY 和 SOURCE_LOT_ID,换成真实收款方,在你的服务端运行即可。每一轮都要把 sep-airdrop 换成新的批次 id,否则第二轮每行都报 duplicate,一个人都不会收到。

Node.js

// npm i viem        (Node 18+, ESM)
import { createHash } from "node:crypto";
import { privateKeyToAccount } from "viem/accounts";

const PAY_API = process.env.PAY_API_URL;        // from your partner kit, no trailing slash
const account = privateKeyToAccount(process.env.PARTNER_PRIVATE_KEY); // the seated wallet
const prefix = account.address.toLowerCase();

// Serialise ONCE. These exact bytes are hashed, signed and sent.
const body = JSON.stringify({
  source_lot_id: process.env.SOURCE_LOT_ID,
  items: [
    { external_ref: `${prefix}:sep-airdrop:u-1`,
      account_ref: { kind: "evm_address", id: "0xAAA…" },
      amount_micros: 10_000_000 },                // $10.00
  ],
});

const path = "/v1/regrants";
const ts = String(Math.floor(Date.now() / 1000));
const bodyHash = createHash("sha256").update(body).digest("hex");
const signature = await account.signMessage({   // EIP-191 personal_sign
  message: `pay-v2|POST|${path}|${ts}|${bodyHash}`,
});

const res = await fetch(PAY_API + path, {
  method: "POST",
  headers: { "Content-Type": "application/json",
             "X-Pay-Timestamp": ts, "X-Pay-Signature": signature },
  body,
});
const out = await res.json();
if (!res.ok) throw new Error(`${res.status} ${JSON.stringify(out)}`); // whole batch refused
for (const it of out.items) console.log(it.external_ref, it.outcome, it.reason ?? "");

Python

# pip install eth-account requests
import hashlib, json, os, time
import requests
from eth_account import Account
from eth_account.messages import encode_defunct

PAY_API = os.environ["PAY_API_URL"]               # from your partner kit, no trailing slash
acct = Account.from_key(os.environ["PARTNER_PRIVATE_KEY"])  # the seated wallet
prefix = acct.address.lower()

# Serialise ONCE. These exact bytes are hashed, signed and sent.
body = json.dumps({
    "source_lot_id": os.environ["SOURCE_LOT_ID"],
    "items": [
        {"external_ref": f"{prefix}:sep-airdrop:u-1",
         "account_ref": {"kind": "evm_address", "id": "0xAAA…"},
         "amount_micros": 10_000_000},            # $10.00
    ],
}, separators=(",", ":")).encode()

path = "/v1/regrants"
ts = str(int(time.time()))
body_hash = hashlib.sha256(body).hexdigest()
signed = acct.sign_message(encode_defunct(text=f"pay-v2|POST|{path}|{ts}|{body_hash}"))
signature = "0x" + bytes(signed.signature).hex()  # EIP-191 personal_sign

res = requests.post(PAY_API + path, data=body, timeout=30, headers={
    "Content-Type": "application/json",
    "X-Pay-Timestamp": ts, "X-Pay-Signature": signature})
res.raise_for_status()                            # whole batch refused
for it in res.json()["items"]:
    print(it["external_ref"], it["outcome"], it.get("reason", ""))

或者在这里签名:用你登录的钱包

用你在本站登录的钱包为一次请求签名,然后自己发出去。本页面不会发出任何请求,也不需要私钥 —— 钱包签完只返回签名。

3. external_ref 与重试

必须以你的钱包地址(小写)加冒号开头,否则该行被拒(external_ref_prefix)。

重试是安全的。引用号会被永久记住:再发一次不会重复付款,而是报 duplicate。超时后原样重发同一批即可。

0xacme…:sep-airdrop:u-1
0xacme…:sep-airdrop:u-2
 └─前缀─┘ └─批次id─┘ └用户id┘        总长度控制在 120 字符以内
错误用法后果
重试时改了引用号的任何一部分会被当作一笔新付款 —— 该用户被付两次
新一轮发放复用上一轮的批次 id每行都报告为已完成,一个人都没收到钱
同一批里重复同一个引用号第二行报告为 duplicate,看起来像凭空少了一行

4. 返回与错误

200 OK
{"granted_count": 2, "duplicate_count": 0, "rejected_count": 0,
 "granted_micros": 20000000,
 "items": [
   {"external_ref": "0xacme…:sep-airdrop:u-1", "outcome": "granted",
    "account_id": "01a0…", "account_created": true, "lot_id": "01a0…",
    "depth": 1, "amount_micros": 10000000,
    "expires_at": "2026-10-15T00:00:00Z"},
   {"external_ref": "0xacme…:sep-airdrop:u-2", "outcome": "granted",
    "account_id": "01a0…", "account_created": true, "lot_id": "01a0…",
    "depth": 1, "amount_micros": 10000000,
    "expires_at": "2026-12-31T00:00:00Z"}   # 未指定,继承了 Lot 的到期日
 ]}

结果逐行返回,失败的行不会回滚已成功的行。每行是以下三种结果之一:

outcome含义该做什么
granted已付无
duplicate这个引用号之前付过了无 —— 这说明重试机制在正常工作
rejected见下方原因视原因而定

拒绝原因

code原因与处理
lot_unusable额度用完了。最常见的一个,它精确对应「钱不够之后的那些行」。找 BD 追加,然后原样重发同一批:已付的报 duplicate,只有差额那些会真正过账。
unknown_account收款方无法创建。钱包会自动创建,邮箱和登录 ID 不行 —— 没有人能证明它们属于谁。改用钱包地址。
scope_widened你的额度锁定在某个服务,这一行却要求了另一个。不填 service 即可继承你的。
external_ref_conflict这个引用号之前用过,但金额或收款方不同。说明拿同一个(批次, 用户)做了不同的付款,给新的那笔换一个批次 id。
external_ref_prefix引用号没有以自己的钱包地址开头。加上前缀重发。
invalid_amount金额为零或负数。

整批失败(非逐行)

状态原因
400发了 X-Actor-Subject
401签名本身不合法,根本恢复不出地址
403 actor_not_authorized恢复出来的地址未被落座为 partner,或已落座但还没有账户(没被发放过额度)。签名串写错也会落到这里:它恢复出的是另一个地址,而那个地址没落座。
404 unknown_lotsource_lot_id 不是你持有的 Lot。在这里填 campaign id 得到的就是它 —— 不是 403,也不是逐行的 lot_unusable。
← 返回 Docs