实现游戏增长 Unity Offerwall Offerwall 变现 自管货币 为 Tapjoy Offerwall 设置自管虚拟货币,包括配置奖励回调以及在自有服务器上处理交易。
使用自管货币直接在您自己的服务器上管理用户的虚拟货币。这种方法相比 Tapjoy 托管货币具有更高的控制权,但也需要您自行处理所有后端流程,包括存储和更新货币余额。
与 Tapjoy 托管货币不同,自管货币不支持 、 或 等函数。此外,对于自管货币,Tapjoy 不会向客户端或应用端发送通知。当 Tapjoy 连接您的回调服务器时,应用和用户的通知由您自行处理。
自管货币有以下独特要求:
奖励延迟:Tapjoy 致力于尽快向用户发放奖励,但无法保证即时到账。为确保货币余额准确更新,请在应用启动、恢复事件、关卡切换、视频广告完成以及商店交互前等情况下定期检查余额变化。请告知用户,完成任务后的奖励可能需要一段时间才会到账。
特定于平台的货币:每个平台(iOS、Android 等)即使采用自管货币,也需要独立设置货币。
回调 URL
当用户通过完成任务获得货币时,Tapjoy 会向您指定的回调 URL 发送 HTTP GET 请求。该请求包含以下参数:
:用户 ID。
:要向用户帐户添加的货币数量。
:用户的 Wi-Fi MAC 地址(如果可用)。
请参阅以下回调示例:
<callback_url>?snuid=<user_id>¤cy=<currency>&mac_address=<mac_address> // Example http://www.sampledoman.com/payments/offers/tapjoy?&snuid=42¤cy=50&mac_address=00-16-41-34-2C-A6
服务器响应要求
您的服务器必须在响应中包含以下 HTTP 状态代码之一:
状态代码 原因 用户已收到其货币。
verifier(校验值)参数与预期值不匹配。
snuid 参数未知。
任何其他阻止进一步重试的错误。
在以下情况下,Tapjoy 会重试请求:
如果 Tapjoy 未收到 或 响应,则会每五分钟重试一次请求,最多持续四天。
如果您的服务器响应时间超过 5 秒,Tapjoy 会将该请求视为失败,并进行重试。
响应必须采用 UTF-8 编码,否则即使服务器返回 ,Tapjoy 也会进行重试。
注意
如果您的服务器返回非 200 的响应,不要向用户发放货币奖励。Tapjoy 会持续重试,而这可能导致重复奖励。
欺诈检测或预防参数
使用虚拟货币密钥可提升安全性。要配置您的密钥,请在 Tapjoy 后台中转到 Monetize(变现) > Virtual Currency(虚拟货币) > Create/Edit(创建/编辑) 。此密钥用于对回调进行签名,不同于应用程序 SDK 密钥。
带有虚拟货币密钥的货币具有以下额外安全参数:
ID:奖励请求的唯一标识符(与 不同)
校验值:ID、snuid、货币和密钥的 MD5 哈希值。
需要关注的欺诈场景
管理货币时,请注意以下欺诈情况:
如果出现重复使用的 ID 或校验值不正确,那么回调 URL 可能不是 Tapjoy 发出的,您可以将其视为欺诈。
如果您的用户 ID 仅包含整数,请验证 snuid 以防止被操控。例如,Tapjoy 会将 和 视为两个不同的用户 ID。但是,您的后端服务器逻辑可能不会这样处理。
校验值计算
校验值是以下格式的 MD5 哈希值:
Digest::MD5.hexdigest("#{id}:#{snuid}:#{currency}:#{secret_key}")
您的服务器必须重新计算校验值,并在任何请求不匹配时返回 响应。请为每个应用程序使用唯一的密钥,以免出现跨应用程序的奖励问题。
每个应用程序必须具有独立的密钥。请勿为所有应用程序使用同一密钥,否则可能导致用户在多个应用程序中同时获得奖励。
改进回调 URL
Tapjoy 支持团队可以提供更安全的回调格式配置。此版本采用 POST 请求,包含 JSON 有效负载,并使用 SHA256 基于哈希的消息身份验证代码 (HMAC) 进行校验。请联系您的客户经理或支持团队来启用这一功能。
当用户获得货币时,Tapjoy 会向您所提供的 URL 发送 POST 请求。Tapjoy 会根据请求正文和您的共享密钥(在 Tapjoy 后台中可以找到)生成校验值,并将其包含在请求标头中。
Tapjoy 按照如下示例结构对请求进行签名:
{ "cp": "some_string", "currency": { "currency_sale":"1.0", "id": "reward_id", "reward":100 }, "id": "some_id", "offer": { "advertiser_app_name": "a_cool_app", "currency": { "max_reward_value":1360 }, "expires_at":1768175428, "icon_url": "some_URL/icon.jpg", "name": "eye_catching_headline", "task":{ "is_iap": false, "name": "event_name" }, "type": "" }, "placement":{ "content_type": "offerwall", "name": "placement_name" }, "rev":100, "timestamp":1762993750, "user": { "id": "user_id" } }
参数 类型 描述
字符串 此请求的唯一奖励 ID。 双精度 获得的收入(以美元为单位)。 字符串 通过 SDK 的 setCustomParameter 方法传递的自定义参数。 字符串 此货币的唯一标识符。 整数 奖励给用户的货币金额。 float 货币促销乘数(如果适用)。 currency.max_reward_value整数 用户完成此次所有任务后可以获得的最大货币量。 字符串 广告主任务的名称。 字符串 目前不支持此参数。 字符串 任务的图标 URL。 offer.advertiser_app_name字符串 广告主的应用名称。 字符串 用户完成并得到奖励的任务的名称或描述。 布尔值 如果奖励事件对应的是 IAP 事件,则值为 true。 timestamp 用户在该时间戳(以秒为单位)之后无法再通过此任务获得奖励。 字符串 与此次转化关联的广告位。 字符串 使用的广告位类型。 字符串 通过 SDK 的 setUserID 方法传递的用户 ID。 timestamp 此次交易的时间戳。
Tapjoy 使用以下格式对请求进行签名:
HMAC_SHA-256(<Request-Body>,<Secret-Key>)
以下示例展示了包含签名的 Tapjoy 标头:
X-Tapjoy-Signature => 7205ccfdfa1fe28cd05a1b56a9508d898cc938aa555a6c18848097fe4ee0975b
设置用户 ID
对于自管货币,在任何 Tapjoy 交互之前设置唯一的 对于确保用户能够收到奖励至关重要。此值在回调 URL 中会变为 snuid。应在初始连接阶段通过 connect 标志设置 ,并确保在发起任何内容请求之前完成该设置。如果设置不正确,用户无法收到奖励,您也无法获得收入。
设置货币的 时,请参阅以下要求:
使用唯一性的数字 ID(最多 190 个字符)。
为符合 GDPR,避免使用个人身份信息(例如:用户名、真实姓名、电子邮箱)。
为确保安全和欺诈检测,保证每个用户的 ID 在其生命周期内保持一致。
如果未设置,Tapjoy 默认使用设备 ID(通常为广告 ID),该 ID 可能因 SDK 版本、设备或操作系统而异。
以下代码示例演示了如何使用 connect 标志,以及必要时如何直接调用 setUserID API(连接后)。直接调用该 API 时,请使用回调来确保已成功设置 ID。最好是尽可能使用 connect 标志。
// Recommended approach using connect flag NSDictionary *connectFlags = @{TJC_OPTION_USER_ID : @"<USER_ID_HERE>"}; [Tapjoy connect:@"SDK_KEY_GOES_HERE" options:connectFlags]; // Setting the user id directly [Tapjoy setUserIDWithCompletion:@"<USER_ID_HERE>" completion:^(BOOL success, NSError *error) { }];
// Recommended approach using connect flag Hashtable<String, Object> connectFlags = new Hashtable<String, Object>(); connectFlags.put(TapjoyConnectFlag.USER_ID, "<USER_ID_HERE>"); // Important for self-managed currency Tapjoy.connect(getApplicationContext(), "SDK_KEY_GOES_HERE", connectFlags, new TJConnectListener() {...}); // Setting the user id directly Tapjoy.setUserID("<USER_ID_HERE>", new TJSetUserIDListener() { @Override public void onSetUserIDSuccess() { } @Override public void onSetUserIDFailure(String error) { } });
// Recommended approach using connect flag Dictionary<string,string> connectFlags = new Dictionary<string,string>(); connectFlags.Add("TJC_OPTION_USER_ID", "<USER_ID_HERE>"); #if UNITY_ANDROID Tapjoy.Connect("your_android_sdk_key", connectFlags); #elif UNITY_IOS Tapjoy.Connect("your_ios_sdk_key", connectFlags); #endif // Callbacks for SetUserID TJPlacement.OnSetUserIDSuccess += HandleOnSetUserIDSuccess; TJPlacement.OnSetUserIDFailure += HandleOnSetUserIDFailure; // Setting the user id directly Tapjoy.SetUserID("<USER_ID_HERE>")
// Recommended approach using connect flag try { let flags: object = { TJC_OPTION_USER_ID: '<userId>' }; await Tapjoy.connect('<sdk_key>', flags); } catch (error) { console.log(error); } // Setting the user id directly try { await Tapjoy.setUserId('<userId>'); } catch (error) { console.log(error); }
您可以在每个平台对应的快速入门页面上找到更多示例。
设置用户余额
每次请求广告位并加载内容之前,都需要更新 Tapjoy 中的用户货币余额。这样可以确保自管货币的追踪结果准确无误。
TJPlacement *placement = [TJPlacement placementWithName:@"placementName" delegate:nil]; [placement setBalance:100 forCurrencyId:@"1234" withCompletion:^(NSError * _Nullable error) { if (error != nil) { //Failure NSString *message = error.localizedDescription; } else { //Success } }];
TJPlacement placement = Tapjoy.getPlacement("placement", this); placement.setCurrencyBalance("1234", 100, new TJSetCurrencyBalanceListener() { @Override public void onSetCurrencyBalanceSuccess() { } @Override public void onSetCurrencyBalanceFailure(int code, String error) { } });
TJPlacement placement = TJPlacement.CreatePlacement("placementName"); placement.SetCurrencyBalance("[CURRENCY_ID]", 100); // Optional callbacks void OnEnable() { TJPlacement.OnSetCurrencyBalanceSuccess += HandleSetCurrencyBalanceSuccess; TJPlacement.OnSetCurrencyBalanceFailure += HandleSetCurrencyBalanceFailure; } void OnDisable() { TJPlacement.OnSetCurrencyBalanceSuccess -= HandleSetCurrencyBalanceSuccess; TJPlacement.OnSetCurrencyBalanceFailure -= HandleSetCurrencyBalanceFailure; } public void HandleSetCurrencyBalanceSuccess(TJPlacement placement) { } public void HandleSetCurrencyBalanceFailure(TJPlacement placement, int code, string error) { }
let placement = new TJPlacement('placementName'); try { await placement?.setCurrencyBalance('1234', 100); } catch (e: any) { let code = e.code; let message = e.message; }
所需金额
如果设置了用户余额,还可以设置广告位所需的金额值。
TJPlacement* placement = [TJPlacement placementWithName:@"placementName" delegate:nil]; placement setRequiredAmount:100 forCurrencyId:@"1234" withCompletion:^(NSError * _Nullable error) { if (error != nil) { //Failure NSString *message = error.localizedDescription; } else { //Success } }
TJPlacement placement = Tapjoy.getPlacement("placement", this); placement.setCurrencyAmountRequired("1234", 100, new TJSetCurrencyAmountRequiredListener() { @Override public void onSetCurrencyAmountRequiredSuccess() { } @Override public void onSetCurrencyAmountRequiredFailure(int code, String error) { } });
TJPlacement placement = TJPlacement.CreatePlacement("placementName"); placement.SetRequiredAmount("[CURRENCY_ID]", 200); // Optional callbacks void OnEnable() { TJPlacement.OnSetCurrencyAmountRequiredSuccess += HandleSetRequiredAmountSuccess; TJPlacement.OnSetCurrencyAmountRequiredFailure += HandleSetRequiredAmountFailure;} void OnDisable() { TJPlacement.OnSetCurrencyAmountRequiredSuccess -= HandleSetRequiredAmountSuccess; TJPlacement.OnSetCurrencyAmountRequiredFailure -= HandleSetRequiredAmountFailure; } public void HandleSetCurrencyBalanceSuccess(TJPlacement placement) { } public void HandleSetCurrencyBalanceFailure(TJPlacement placement, int code, string error) { } public void HandleSetRequiredAmountSuccess(TJPlacement placement) { } public void HandleSetRequiredAmountFailure(TJPlacement placement, int code, string error) { }
let placement = new TJPlacement('placementName'); try { await offerwallPlacement?.setRequiredAmount(100, '100'); } catch (e: any) { let code = e.code; let message = e.message; }
奖励回调 IP 白名单
如果您的回调服务器会限制访问,请将以下 Tapjoy IP 地址加入白名单:
注意
此列表截至 2024 年 5 月 12 日(星期日)准确无误。
故障排除
如果在回调 URL 中收到异常的 snuid,请确保每次应用启动时,在 connect 调用之后,用户访问任务之前,调用 。
如果设备没有关联的用户 ID,Tapjoy 将在回调 URL 中发送 snuid 形式的设备 ID。例如,用户可能启动应用后就导航到 Tapjoy 网站,此时您的应用尚未发送 userID。为了防止这种情况,请确保服务器在每次应用启动时进行 connect 调用后都调用 。
如果未设置用户 ID,系统将尝试使用可用的最优设备 ID。通常情况下,这个 ID 值就是设备的广告 ID。但是,根据 SDK 版本、设备型号/版本、设备操作系统 (OS) 版本和 Google Play 服务,具体的 ID 可能会有所不同。其他可能的值包括 Android ID、udid 和 mac_address。