JUTERA DocsSign in Get keys

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.

Get your keys

High-Level Integration Steps

  1. DashboardCopy your keys

    One key pair for both Jutera products. pk_… ships in the client; sk_… stays on your server.

  2. Your serverMint a search launch token

    Sign one token per user with product: "search". The secret never reaches the browser.

  3. Web or appOpen the redirect

    New tab or same-tab on the web; the webview top document in an app. Never an iframe.

  4. Your serverCredit from the webhook

    Receive search.redirected, verify the signature, credit pointcoins once per event_id when counted is true.

  5. DashboardGet certified

    Upload a video of the real experience. Monetized results start after staff approve it.

1 · At a glance

AIRewards SDKSearchRewards SDK
User actionAsk, compare two AI answers, pick plus a reasonSubmit a web search
Surface Jutera rendersChat widget in an iframe or webviewNone. Jutera only redirects
Reward nameYour own currencyPointcoins
Real-time signalreward.earned webhooksearch.redirected webhook, no dollars
How you get paidShare of ad revenue per eventShare 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)

KeyWhere it livesWhat it does
pk_… publishableClient code, snippets, mobile appsIdentifies your publisher account. Safe to ship.
sk_… secretYour server only (env var)Signs launch tokens; authenticates the Availability and Reporting APIs. Shown once.
whsec_… webhook secretYour server onlyVerifies 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 − iat must 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.

ContainerHow
iOS WKWebViewwebView.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 WebViewwebView.loadUrl(goUrl). Allow navigation from the Jutera host to search.yahoo.com in your WebViewClient.
React Native WebViewSet source={{ uri: goUrl }} as the root source. Do not wrap it in a local HTML page.
Flutter webview_fluttercontroller.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 with counted: false and a reason — 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_detection
detected, not_detected, or unavailable (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.

CodeWhereMeaning
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), AvailabilitySearchRewards paused or disabled for this publisher. No Yahoo redirect.
user_ineligible/sr/go (403), AvailabilityThis user is banned from search for this publisher.
geo_blocked/sr/go (403), AvailabilityThe user's IP is in the European Union; SearchRewards is not offered there. No Yahoo redirect, no webhook.
unauthorizedAvailability, Reporting (401)Bad or missing sk_….
not_foundReporting lookup (404)No such event for your publisher.