Add rewarded web search in just a few minutes
The SearchRewards SDK by Jutera lets your users search the web and earn your pointcoins. Your server mints a short-lived token, your page or app opens one redirect URL, Yahoo shows the results, and your server credits pointcoins from a signed webhook. There is no widget to embed and nothing to skin. Giving this to your agent? Point them to /llms.txt.
High-Level Integration Steps
- DashboardCopy your keys
One key pair for both Jutera products.
pk_…ships in the client;sk_…stays on your server. - Your serverMint a search launch token
Sign one token per user with
product: "search". The secret never reaches the browser. - Web or appOpen the redirect
New tab or same-tab on the web; the webview top document in an app. Never an iframe.
- Your serverCredit from the webhook
Receive
search.redirected, verify the signature, credit pointcoins once perevent_idwhencountedis true. - DashboardGet certified
Upload a video of the real experience. Monetized results start after staff approve it.
1 · At a glance
| AIRewards SDK | SearchRewards SDK | |
|---|---|---|
| User action | Ask, compare two AI answers, pick plus a reason | Submit a web search |
| Surface Jutera renders | Chat widget in an iframe or webview | None. Jutera only redirects |
| Reward name | Your own currency | Pointcoins |
| Real-time signal | reward.earned webhook | search.redirected webhook, no dollars |
| How you get paid | Share of ad revenue per event | Share of affiliate revenue, next day, by sub-id |
Jutera never holds an end-user pointcoin balance. The pointcoin amount is yours to choose; Jutera does not send an amount in the webhook. Redirect count and paid revenue will diverge: a redirect can bounce, be non-bidded, or fail traffic quality. You carry pointcoin liability for redirects; Jutera pays only settled affiliate revenue after the revenue share.
2 · Keys (same pair)
| Key | Where it lives | What it does |
|---|---|---|
pk_… publishable | Client code, snippets, mobile apps | Identifies your publisher account. Safe to ship. |
sk_… secret | Your server only (env var) | Signs launch tokens; authenticates the Availability and Reporting APIs. Shown once. |
whsec_… webhook secret | Your server only | Verifies webhook signatures for both products. Shown masked after creation. |
Never put sk_… or whsec_… in client code, repos, or logs. The publishable key alone cannot open the search redirect — an unsigned pk plus a user id would let anyone mint traffic against your account, so the signed token below is required.
3 · Launch token with product: "search" (your server)
Sign it on your server, never in the browser. One token per end-user search session. Format (JWT-shaped, HS256):
base64url({"alg":"HS256","typ":"JWT"}) + "." + base64url(payload) + "." + base64url(HMAC_SHA256(signing_input, sk_…))
{
"pk": "pk_…",
"user_id": "your-user-123",
"metadata": { "placement": "earn_tab" },
"test_mode": false,
"product": "search",
"iat": 1760000000,
"exp": 1760000600
}
productRequired for search- Must be
"search". A token minted without it — no product, or"ai"— cannot open/sr/go; SearchRewards requires its own token. user_idRequired- An opaque, stable id for this user, at most 128 characters. Caps, the sub-id, and reporting key on it. Never an email or a name.
metadata.placementRequired- Where you offer search, such as
earn_tab. Use 1–32 characters: letters, numbers, underscore, and hyphen. Echoed on the webhook and in reports. test_modeOptional- When true, searches use the naked Yahoo URL (no partner identifiers) and are never monetized, even after certification.
expExpires at · required- Unix seconds.
exp − iatmust be at most 900 seconds (15 minutes). The samples use 600 seconds.
Node.js
const crypto = require('crypto');
const b64 = (obj) => Buffer.from(JSON.stringify(obj)).toString('base64url');
function launchToken({ userId, metadata = {}, testMode = false }) {
const now = Math.floor(Date.now() / 1000);
const header = b64({ alg: 'HS256', typ: 'JWT' });
const payload = b64({
pk: process.env.SEARCHREWARDS_PK,
user_id: String(userId),
metadata,
test_mode: testMode,
product: 'search',
iat: now,
exp: now + 600,
});
const input = header + '.' + payload;
const sig = crypto.createHmac('sha256', process.env.SEARCHREWARDS_SK).update(input).digest('base64url');
return input + '.' + sig;
}
Python
import base64, hashlib, hmac, json, os, time
def _b64(obj):
raw = json.dumps(obj, separators=(',', ':')).encode()
return base64.urlsafe_b64encode(raw).rstrip(b'=').decode()
def launch_token(user_id, metadata=None, test_mode=False):
now = int(time.time())
header = _b64({'alg': 'HS256', 'typ': 'JWT'})
payload = _b64({
'pk': os.environ['SEARCHREWARDS_PK'],
'user_id': str(user_id),
'metadata': metadata or {},
'test_mode': test_mode,
'product': 'search',
'iat': now,
'exp': now + 600,
})
signing_input = header + '.' + payload
sig = base64.urlsafe_b64encode(
hmac.new(os.environ['SEARCHREWARDS_SK'].encode(), signing_input.encode(), hashlib.sha256).digest()
).rstrip(b'=').decode()
return signing_input + '.' + sig
PHP
<?php
function searchrewards_token($userId, $metadata = [], $testMode = false) {
$b64 = function ($obj) {
$json = json_encode($obj);
return rtrim(strtr(base64_encode($json), '+/', '-_'), '=');
};
$now = time();
$header = $b64(['alg' => 'HS256', 'typ' => 'JWT']);
$payload = $b64([
'pk' => getenv('SEARCHREWARDS_PK'),
'user_id' => (string) $userId,
'metadata' => (object) $metadata,
'test_mode' => $testMode,
'product' => 'search',
'iat' => $now,
'exp' => $now + 600,
]);
$input = $header . '.' . $payload;
$sig = rtrim(strtr(base64_encode(hash_hmac('sha256', $input, getenv('SEARCHREWARDS_SK'), true)), '+/', '-_'), '=');
return $input . '.' . $sig;
}
4 · Web design guidelines: new tab or same-tab redirect
Your button opens https://search.pointcoin.co/sr/go?token=…&q=…. Pick one of these two. Both are fine.
New tab
searchButton.addEventListener('click', () => {
const url = 'https://search.pointcoin.co/sr/go?token=' + encodeURIComponent(token) + '&q=' + encodeURIComponent(query);
window.open(url, '_blank', 'noopener');
});
Your page stays put; the new tab lands on Yahoo.
Same-tab redirect
searchButton.addEventListener('click', () => {
window.location.href = 'https://search.pointcoin.co/sr/go?token=' + encodeURIComponent(token) + '&q=' + encodeURIComponent(query);
});
The user leaves your page and returns with the browser Back button.
Credit always comes from the search.redirected webhook, never from watching the results page load.
Ad-blocker gate. Before results open, Jutera briefly checks for an ad blocker (search revenue is paid from ads on the results page). With a blocker detected, the user is asked to turn it off and retry — no search, no credit. Design your earn surface so that message makes sense: tell users up front that search earning needs the blocker off. Browsers without JavaScript continue without the check and are recorded as adblock_detection: "unavailable".
5 · App design guidelines: webview as the top document
A webview is not an iframe. Loading https://search.pointcoin.co/sr/go?token=…&q=… as the webview's main document (loadUrl / load(URLRequest)) follows the redirect to Yahoo as a top-level navigation, so the frame headers do not apply. SFSafariViewController and Android Custom Tabs are also top-level and fine.
| Container | How |
|---|---|
| iOS WKWebView | webView.load(URLRequest(url: goUrl)) as the main document. If WKAppBoundDomains is in use, list both the Jutera host and search.yahoo.com, or the redirect dies inside the app. |
| Android WebView | webView.loadUrl(goUrl). Allow navigation from the Jutera host to search.yahoo.com in your WebViewClient. |
| React Native WebView | Set source={{ uri: goUrl }} as the root source. Do not wrap it in a local HTML page. |
| Flutter webview_flutter | controller.loadRequest(Uri.parse(goUrl)) as the initial load. |
Do not load a local HTML shell and put the Yahoo URL in an iframe inside it — that inner frame is an iframe and Yahoo refuses it. The webview must be on screen and opened by a user action; a hidden webview that fires searches will be rejected at certification. Result taps can stay in the webview or hand off to the system browser; that choice does not affect the redirect or the webhook. There is no widget bridge and no earn event in the webview — the webhook is the earn signal.
6 · Webhook search.redirected and signature verification
After an accepted redirect, Jutera POSTs search.redirected to the same webhook URL and secret you use for AIRewards.
AIRewards-Signature: t=1760000123,v1=5257a869…
AIRewards-Event-Id: evt_… (same id as the body's "event_id")
{
"event_id": "evt_…", "type": "search.redirected",
"publisher_id": "pub_…", "user_id": "your-user-123",
"metadata": { "placement": "earn_tab" },
"query_hash": "9f86d081…", "vsub": "…",
"test_mode": false, "monetized": true, "counted": true, "reason": null,
"created_at": "2026-10-17T12:00:05Z"
}
counted- Credit pointcoins only when this is
true. Over-cap, repeat, and too-frequent searches arrive withcounted: falseand areason— do not credit them. monetized- True when the user went to the paid Yahoo tag. False still credits (when counted): the user saw a normal Yahoo results page, and the event will never appear on an affiliate revenue row.
query_hash- SHA-256 of the normalized query. The webhook never includes the raw query — you already have it from your own UI.
reason- Null on a counted monetized redirect; otherwise one of
daily_cap_reached,repeat_query,too_frequent,geo_unavailable,test_mode,not_live,kill_switch,adblock_detected(soft gate only; the hard gate sends no webhook). adblock_detectiondetected,not_detected, orunavailable(no JavaScript check ran).
Verify the signature exactly as for reward.earned — the header and HMAC scheme are identical. The copy-paste verifiers for Node, Python, and PHP are in the AIRewards guide. Respond with any 2xx within 10 seconds; dedupe on event_id (retries reuse it, at-least-once for ~24 hours). There are no money fields: what you credit is your own pointcoin logic. Fastest check: Send test search.redirected on the dashboard Webhook page.
7 · Availability
Call it server-side before you show the search entry point, so you can hide it when the user is capped or outside the monetized countries. Skipping it is allowed; /sr/go still enforces every rule.
GET https://jutera.com/v1/availability?product=search&user_id=your-user-123[&user_ip=203.0.113.9]
Authorization: Bearer [REDACTED]…
200
{
"eligible": true,
"reason": "available",
"cache_ttl_seconds": 60,
"product": "search"
}
Reasons: available, daily_cap_reached, publisher_paused, user_ineligible, geo_unavailable, geo_blocked (EU — do not offer search). The optional user_ip is used only for the geo decision — availability is called from your server, whose IP is not your user's. The redirect itself always uses the real user IP and is authoritative.
8 · What pointcoins are
The reward for a counted search is called pointcoins in your product. Each publisher decides what a pointcoin means inside their own app; there is no shared balance across publishers and no pointcoin account at Jutera.
Steer the value toward a booster or an unlock: for example, earn N pointcoins over a window for a 2x multiplier, or earn N pointcoins to open a gift-card tier. Do not publish a cash price per pointcoin or per search, and do not pay users to click Yahoo ads. Publishers who treat every redirect as cash-equivalent overpay themselves: a redirect can be non-bidded or fail traffic quality, and only settled affiliate revenue is paid out.
9 · Reporting lag
Redirect counts can be same-day. Revenue cannot: Our affiliate reporting provider delivers a nightly report, and Jutera matches each row to a publisher by sub-id. Revenue responses always carry as_of set to the report date, sit in a 7-day hold (your account setting), then become payable on the same payout as AIRewards revenue.
GET https://jutera.com/v1/search/reports/daily?from=2026-10-11&to=2026-10-17
GET https://jutera.com/v1/search/reports/lookup?user_id=your-user-123
GET https://jutera.com/v1/search/reports/lookup?event_id=evt_…
Daily rows carry redirects, counted events, bidded searches, revenue cents, and traffic quality (TQ) for the report date. Your dashboard Search reports show the same split by day; the placement breakdown (group_by=metadata.placement) is available on this API.
10 · Certification and go-live
Signing up changes nothing about how search behaves today: until certification, /sr/go sends users to a naked Yahoo results URL with no partner identifiers (https://search.yahoo.com/search?p=…). After staff approve your implementation, live traffic uses the monetized URL (https://search.yahoo.com/yhs/search?hspart=litmus&hsimp=yhs-002&type=…-vsub-…&p=…). test_mode stays naked even after approval.
To request review from Dashboard → SearchRewards → Certification, upload a video (mp4 or webm, 200 MB maximum) of the real experience: a user-initiated search, the Yahoo results page, and the pointcoin credit that follows the webhook. Confirm every checklist item — user-initiated search, pointcoins naming with no cash rate, no pay-per-ad-click, visible daily cap and repeat-search rule — with terms accepted, payout details present, and one naked redirect with a 2xx webhook on record. Staff watch the video before approving, because farmed searches damage traffic quality for everyone. A rejection returns you to integrating with staff notes; fix the issue and resubmit.
11 · US-only monetized traffic; EU blocked
Monetized traffic is US-only for now. A non-US, non-EU user on a live publisher still reaches naked Yahoo and you still get a search.redirected webhook with monetized: false and reason: "geo_unavailable", so your app keeps working while other countries are checked. Availability returns geo_unavailable for those users so you can hide the earning entry. Widening the country list is a Jutera settings change, not a code change on your side.
EU users are blocked entirely. When a user's IP is determined to be in the European Union, /sr/go refuses with geo_blocked — no Yahoo redirect and no webhook — and Availability returns geo_blocked. Do not offer the search entry point to EU users.
12 · Errors
A bad redirect call does not reach Yahoo and does not fire a webhook. The browser gets a short Jutera error page carrying the machine code; API calls keep the { "error": { "code": "…" } } envelope.
| Code | Where | Meaning |
|---|---|---|
invalid_token | /sr/go (401) | Missing, malformed, wrong-secret, or non-search token. |
token_expired | /sr/go (401) | Token past exp, or lifetime over 15 minutes. |
missing_query | /sr/go (400) | No q parameter. |
missing_placement / invalid_placement | /sr/go (400) | metadata.placement absent or outside 1–32 of letters, numbers, underscore, hyphen. |
publisher_paused | /sr/go (403), Availability | SearchRewards paused or disabled for this publisher. No Yahoo redirect. |
user_ineligible | /sr/go (403), Availability | This user is banned from search for this publisher. |
geo_blocked | /sr/go (403), Availability | The user's IP is in the European Union; SearchRewards is not offered there. No Yahoo redirect, no webhook. |
unauthorized | Availability, Reporting (401) | Bad or missing sk_…. |
not_found | Reporting lookup (404) | No such event for your publisher. |