GPC / DEVELOPER DOCS

广告 API 与业务回调

业务使用统一广告位和签名回调,自行决定并发放奖励。

当前状态

广告功能保持关闭,服务端返回 AD_REWARDS_DISABLED。Android 427 基座含穿山甲、优量汇;iOS 427 还包含快手。SDK 尚无真实平台准入、广告位或回调联调结果;此文档发布不启用广告。旧客户端必须检测 API 与能力状态。

接入资料

职责与身份

DApp 只传业务广告位和凭证,不传用户、SDK、平台广告位、奖励金额或收款地址。当前身份由 Wallet 宿主从自己的登录凭据确认,平台选择由框架处理。业务服务端负责资格、业务动作、风控和奖励发放;框架只核验广告证明并可靠通知业务。

标识 / 密钥用途与保管位置
业务 App ID框架内的业务接入标识,用于凭证申请与回调归属;不是 SDK App ID。
广告平台 App ID运营商在穿山甲、优量汇、快手申请并获准的宿主应用标识,由可信宿主配置。
业务 API Key只存业务服务端,不能放网页或钱包客户端。
业务回调密钥独立随机密钥,用来验证框架通知,只存服务端。
平台回调凭据用于核验平台证明,与业务回调密钥不同,只存框架服务端。

1. 业务服务端申请凭证

POST https://api.gpcchat.com/api/ad-rewards/apps/YOUR_BUSINESS_APP_ID/tickets
Authorization: Bearer YOUR_SERVER_ONLY_API_KEY
Content-Type: application/json

{
  "placement": "bonus",
  "requestId": "unique-business-action-001",
  "context": "unique-business-action-001"
}

同一业务动作重试必须使用同一 requestId,不能每次生成新值。context 使用业务动作 ID,不放敏感信息。成功后保存 data.rewardTicket 与业务动作的对应关系,再将凭证交给网页。请求超时重试不会自动延长旧票据有效期。

2. DApp 主动调用广告

const ads = window.gpc?.ads;
if (!ads || !(await ads.getCapabilities()).available) {
  // 提示广告暂不可用,返回正常业务界面。
  return;
}
// 由业务服务端校验会话、资格和业务动作,再申请并保存凭证。
const { rewardTicket } = await fetch('/ads/tickets', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ actionId, placement: 'bonus' }),
}).then(r => { if (!r.ok) throw new Error('凭证申请失败'); return r.json(); });
try {
  await ads.show({ placement: 'bonus', rewardTicket });
} finally {
  // 包括关闭、取消或超时:向自己的业务服务端查询状态。
  // 客户端播放结果不作为发奖依据。
  await refreshBusinessRewardStatus(actionId);
}

示例中的 /ads/tickets 和奖励查询属于业务自己的接口,并非框架现成服务。可选预加载接口为 ads.load({ placement, rewardTicket });多平台模式不带凭证时返回 deferred,没有调用真实 SDK。播放完成只表示待服务端核验,不表示奖励已到账。

3. 回调验签

框架 POST 到预登记的 HTTPS 地址。使用三个头:X-GPC-Reward-Event-ID、X-GPC-Reward-Timestamp、X-GPC-Reward-Signature。签名格式为 v1=<小写 hex HMAC-SHA256>,原文是时间戳、事件 ID、原始 body,以英文句点分隔。

import { createHmac, timingSafeEqual } from 'node:crypto';
// rawBody 必须是原始 HTTP 字节;不要先解析 JSON 再序列化。
const expected = createHmac('sha256', callbackSecret)
  .update(timestamp + '.' + eventId + '.')
  .update(rawBody)
  .digest();
// 先严格检查 v1=<64位小写hex>、单值签名头及时间戳,再解码。
if (actual.length !== expected.length || !timingSafeEqual(actual, expected)) {
  throw new Error('回调签名无效');
}
// 验证前后 300 秒、预期业务 App ID / origin / eventId;
// 验证通过后再解析 body 并执行数据库幂等事务。
// 完整检查和事务实现请下载示例,不要仅复制此片段上线。

回调包含业务 appId、eventId、ticketId、宿主绑定的钱包身份、origin、placement 和 context。必须验证预期归属,并与自己已保存的业务动作匹配;不要信任网页上报的完成事件。

4. 幂等与奖励任务

  1. 以 eventId 和 ticketId 建唯一约束,业务动作也只能生成一次奖励任务。
  2. 在同一数据库事务中记录事件、核对归属并写入业务任务;提交成功才返回 HTTP 2xx。
  3. 重复通知确认内容一致后返回成功;事务失败返回失败,让框架重试。
  4. 链上或外部奖励另用业务 outbox、唯一业务单号及交易回查控制重复执行,不能仅靠 HTTP 回调去重防止重复转账。

框架至少一次投递,只有 HTTP 2xx 算确认,3xx 不跟随。响应丢失可能导致重试;同一事件 body 不变,时间戳和签名更新。奖励完全由业务处理,框架没有领奖或转账接口。

广告账号与宿主配置

当前由 Wallet 宿主统一配置广告平台账号,暂不提供多运营商自助接入独立 SDK App ID 或按请求切换 App ID。广告平台 App ID 与业务接入 App ID 分开管理,业务接入方无需申请或提交 SDK App ID。宿主平台准入和广告位尚未配置,广告保持关闭。

同一进程内每家 SDK 首次初始化的 App ID 会固定,不能按每次广告请求在多个运营商 App ID 间随机切换。广告位分流不等于 SDK App ID 切换;账号与宿主的配置方式须在实际启用前确认。Android 不支持快手,不能将其加入 Android 的流量池。

平台初始化与绑定说明参考穿山甲官方初始化文档和应用登记说明。业务网页始终无需关心 SDK 或提交平台凭据。