Unity Offerwall Offerwall 变现 自管货币 为 Tapjoy Offerwall 设置自管虚拟货币,包括配置奖励回调以及在自有服务器上处理交易。
当您使用自有服务器来处理用户货币时,需采用自管货币模式。此模式可让您更全面掌控用户货币,但也意味着您需要全权负责后端货币管理工作,包括存储和处理所有用户的货币金额。getCurrencyBalance、awardCurrency 或 spendCurrency 仅适用于托管货币 模式。对于自管货币,Tapjoy 不会向客户端/应用端发送通知;当我们调用您的回调服务器时,应用和用户的通知由您自行处理。您必须设置一个回调服务器才能使用自管货币。
尽管 Tapjoy 会尽最大努力尽快为用户发放奖励,但不能保证用户立即收到奖励。用户收到奖励的时间受很多因素影响。最好是定期检查用户的最新余额,并在应用发生某些事件时进行此检查,例如在应用启动后、应用恢复运行后、关卡切换期间、视频广告关闭后、商店加载前等。此外,建议告知用户,完成任务后可能需要一些时间才能收到奖励。
注意
每个平台都必须使用独立的货币(即便是自管货币)。
回调 URL
当用户完成任务获得虚拟货币后,我们将向此 URL 发起一次 HTTP GET 请求。参数的格式如下:
<callback_url>?snuid=<user_id>¤cy=<currency>&mac_address=<mac_address> //Example http://www.sampledoman.com/payments/offers/tapjoy?&snuid=42&currency=50&mac\_address=00-16-41-34-2C-A6
默认的请求参数包括 snuid 和 currency(表示应为用户帐户增加的货币数量)以及用户的 WiFi mac_address(如果可用)。
服务器响应
Tapjoy 服务器期望服务器返回 或 响应。
服务器响应 案例 - 用户成功获得货币。
verifier 参数与计算出的值不匹配。
系统无法识别 snuid 参数。
存在其他不应重试的错误。
如果 Tapjoy 服务器收到 200 或 403 以外的任何其他响应,Tapjoy 将继续重试。
重要
如果收到 200 以外的响应,不要向用户发放奖励。由于 Tapjoy 将继续重试回调,因此手动向用户发放奖励可能会导致大量重复奖励。
Tapjoy 每 5 分钟左右重试一次回调,持续 4 天。如果服务器的响应时间超过 5 秒,Tapjoy 的算法会将该请求视为失败。
注意
回调 URL 响应的响应正文需要采用 UTF-8 编码格式。如果不采用 UTF-8 格式,即使返回 200,我们也会重试。
用于检测和预防欺诈的可选参数
您可以在 Dashboard(后台) > Monetize(变现) > Virtual Currency(虚拟货币) > Create/Edit(创建/编辑) 中访问虚拟货币密钥。此密钥与应用程序 SDK 密钥不同。此虚拟货币密钥用于对回调进行签名。
如果货币中存在密钥 (),我们会向回调请求中添加以下参数:
参数 描述 注意 这是具有唯一性的标识符,用于标记奖励的特定货币。 表示 而不是 。 ID、snuid、货币和密钥的 MD5 哈希值。
应始终注意的欺诈情况
如果您发现 ID 被重用和/或校验值不正确,表示回调 URL 不是来自 Tapjoy,应视为欺诈。
如果您的用户 ID 仅包含整数,建议您执行简单的验证以确保用户 ID 未被篡改。例如,Tapjoy 将“001234”和“1234”视为两个不同的用户 ID,但后端服务器的逻辑可能并非如此。
校验值
校验值 (verifier) 的计算方法是用冒号将 ID、snuid、货币和密钥连接成一个字符串,然后对该字符串计算 MD5 哈希值。在 Ruby 代码中,如下所示:
Digest::MD5.hexdigest("#{id}:#{snuid}:#{currency}:#{secret_key}")
您的服务器应重新计算校验值,并拒绝任何不匹配的请求。如果校验值不匹配,服务器应返回 403 Forbidden 响应。
注意
每个应用程序应具有独立的密钥。请勿对所有应用程序使用相同的密钥,否则可能导致向一个应用程序中的用户发放奖励也会发放给另一个应用程序中的用户。此外,上面的 ID 表示 request_id 而不是 currency_id。
改进回调 URL
我们对上面列出的回调进行了改进,提供更多信息和更高安全性。如果希望使用此改进版本,请联系您的客户经理或我们的支持团队,他们会帮助您启用此版本的回调。
用户通过完成任务赚取货币后,我们将向您提供的 URL 发起一次 POST 请求。参数的格式如下:
{ "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 方法传递的自定义参数 字符串 此货币的唯一标识符 整数 奖励给用户的货币金额 浮点 货币促销乘数(如果适用) currency.max_reward_value整数 用户完成此次所有任务后可以获得的最大货币量。 字符串 广告主提供的任务的名称 字符串 目前不支持此参数 字符串 任务的图标 URL offer.advertiser_app_name字符串 广告主的应用名称 字符串 用户完成并得到奖励的任务的名称或描述。 布尔值 如果奖励事件对应的是 IAP 事件,则值为 true 时间戳 用户在该时间戳(以秒为单位)之后无法再通过此任务获得奖励。 字符串 与此次转化关联的广告位 字符串 使用的广告位类型 字符串 通过 SDK 的 setUserID 方法传递的用户 ID 时间戳 此次交易的时间戳
JSON 正文将首先进行递归排序,然后用 SHA256 哈希算法进行哈希计算。通过 POST 货币回调请求正文和我们的共享密钥(在 Tapjoy 后台上通过您的回调 URL 可以找到此密钥)将生成校验值。此校验值将置于 POST 货币回调请求标头中发送。
HMAC_SHA-256(<Request-Body>,<Secret-Key>)
包含签名的 Tapjoy 标头示例:
X-Tapjoy-Signature => 7205ccfdfa1fe28cd05a1b56a9508d898cc938aa555a6c18848097fe4ee0975b
注意
参数值的字符数上限为 300。字符数超出此上限时,Tapjoy 会将其截断。
用户 ID
如果您使用自管货币,那么设置用户 ID 就至关重要。此值是回调 URL 中设置的 值。在请求任何内容之前,建议使用连接标志在连接时设置用户 ID。
如果设置不正确,您的用户将不会得到奖励,您也将不会得到报酬。应设置具有唯一性的用户 ID(通常为数字)。
为保障数据安全和 GDPR 合规性,setUserID 参数不应包含任何可识别或可追溯个人身份的信息,例如用户名、真实姓名或电子邮件地址。 出于安全防护与欺诈检测目的,用户 ID 应在用户整个生命周期内保持不变。(例如,请勿使用用户 ID 参数来披露用户等级或分数等信息。)
用户 ID 最长为 190 个字符。
下面为不同平台提供了相应的代码示例,演示了如何使用连接标志,以及如何在必要时直接调用 API(连接后)。直接调用 API 时,请使用回调确保已成功设置 ID。强烈建议尽可能使用连接标志:
// 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); }
您可以在 iOS 、Android 、Unity 和 React Native 平台对应的快速入门页面上找到更多示例。
故障排除: 如果要调用 setUserID,但回调 URL 中的 snuid 不是预期值,则需在用户进入应用内 Offerwall 或 tapjoy.com 完成任务之前调用 setUserID。如果设备没有关联 userID,Tapjoy 将在回调 URL 中将设备 ID 作为 snuid 发送。例如,用户可能启动应用后就转到 tapjoy.com,但此时该应用尚未向我们发送 userID。为了防止这种情况,您需要确保在每次启动应用时进行 connect 调用后都调用 setUserID。
如果未设置用户 ID,系统将尝试使用可用的最优设备 ID。在大多数情况下,这个 ID 值就是设备的广告 ID。但是,根据 SDK 版本、设备型号/版本、设备操作系统版本和 Google Play 服务,具体的 ID 可能会有所不同。其他可能的值包括 Android ID、udid 和 mac_address。
设置用户余额
每次请求广告位时,您都可以将用户的当前余额告知 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 } }];
let placement = TJPlacement(name: "placementName", delegate: nil) placement?.setBalance(100, forCurrencyId:"1234", withCompletion: { error in if let error = error { //Failure let 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; }
_offerwallPlacement?.setCurrencyBalance( currencyBalance: balance, currencyId: currencyId, onSuccess: (placement) { }, onFailure: (placement, error) { });
所需金额
如果设置了用户余额,还可以设置广告位所需的金额值。
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 } }];
let placement = TJPlacement(name: "placementName", delegate: nil) placement?.setRequiredAmount(100, forCurrencyId:"1234", withCompletion: { error in if let error = error { //Failure let 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; }
_offerwallPlacement?.setRequiredAmount( requiredAmount: requiredAmount, currencyId: currencyId, onSuccess: (placement) { }, onFailure: (placement, error) { });
奖励回调 IP 白名单
如果您的奖励回调服务器需要额外权限(白名单)才能访问,下面是 Tapjoy 的 IP 地址列表。(最后修改时间:2024 年 5 月 12 日)
18.215.207.89 18.235.142.165 23.20.255.113 23.23.134.165 3.210.188.32 3.215.42.140 3.217.209.177 3.218.95.35 3.219.236.53 3.231.137.161
从 Tapjoy 托管货币切换到自管货币
如果您的应用尚未发布,可以在 Tapjoy 后台的 Edit Virtual Currency(编辑虚拟货币)屏幕上,从 Tapjoy 托管货币切换到自管货币。请务必在回调 URL 字段中输入格式正确的 URL,否则更改将无法生效。
如果您的应用程序已上线并采用 Tapjoy 托管货币,切换过程会更加复杂。这种情况下,请注意以下事项:
您需要创建新应用并生成新的 SDK 密钥。这样可以确保使用旧版应用(未升级到最新版本)的用户不受影响。如果您在采用自管货币的应用中继续使用原来的 SDK 密钥,则采用 Tapjoy 托管货币的应用中的所有用户将无法获得完成任务的奖励。
您需要禁用所有指向旧 App ID 的广告系列,并重新创建广告系列并使其指向新 App ID。
切换到自管货币还需要更改应用中的代码。您将无法使用 getCurrencyBalance、awardCurrency 或 spendCurrency,因为它们仅适用于托管货币。
建议在考虑进行更改之前联系您的客户经理。
无法从自管货币切换到 Tapjoy 托管货币。因此,请务必在更改之前三思。
要迁移用户的余额,建议按照以下步骤操作:
首次发布应用时,使用旧的 SDK 密钥(采用 Tapjoy 托管货币),并调用 getCurrencyBalance 来获取余额。
将余额更新为 getCurrencyBalance 返回的值。
在所有后续发布时,使用新的 SDK 密钥。
如果您正在考虑切换到自管货币,但不确定如何实现自己的虚拟货币服务器,建议您查看 Parse 或 UrbanAirship 的解决方案。