skill prompt
# Verse8 Ads — agent implementation guide
Two surfaces for two audiences. Implement the client side first; add server-side verification only when the reward is valuable.
## SDK (`@verse8/ads`) — client side
Install:
```bash
pnpm add @verse8/ads
```
Show a rewarded ad. Trigger from a user gesture (click/tap); never auto-show on page load.
```tsx
import { Verse8Ads } from "@verse8/ads";
const result = await Verse8Ads.showRewarded({ placementId: "revive-hero" });
switch (result.status) {
case "rewarded":
// User watched the ad. Grant the in-game reward.
// (Optionally verify server-side first — see below.)
grantRewardLocally();
break;
case "dismissed":
ui.toast("Watch the full ad to earn the reward");
break;
case "failed":
if (result.error.code === "busy") return; // ignore — disable button while busy
if (result.error.code === "unsupported_env") return; // hide ad UI for the session
ui.toast("Ad unavailable, please try again later");
break;
}
```
Result shape:
```ts
type AdResult =
| { status: "rewarded"; reward: { amount: number; type: string }; requestId: string }
| { status: "dismissed"; requestId: string }
| { status: "failed"; error: { code: "busy" | "timeout" | "unsupported_env" | "platform_error"; message?: string }; requestId: string };
```
Interstitials follow the same call shape (`Verse8Ads.showInterstitial({ placementId })`) but never grant rewards — don't branch on the result.
Rules:
- Trigger from a user gesture. Never auto-show.
- Disable the button while busy — the SDK only shows one ad at a time.
- Hide the button when `unsupported_env` arrives.
- Pin a specific version (`@0.2.0`); never `@latest`.
- `result.reward.amount` is a UX hint, not the grant amount.
## Server-side verification (optional)
Skip for cosmetic / low-stakes rewards. Implement for currency, premium items, rare drops.
> **Coming soon:** native Agent8 GameServer SDK helper for server-side ad verification. Until then (or if you run your own game server), use the flow below.
Endpoint (no auth, read-only):
```
GET https://ads-verifier.verse8.io/ads/status?requestId=<requestId>
```
| `body.status` | Action |
|---|---|
| `verified` | Grant the reward |
| `dismissed` / `failed` | Don't grant |
| `pending` (HTTP 202) | Retry shortly with bounded budget |
Server pattern (any fetch-capable runtime):
```ts
const REWARD_TABLE = {
"revive-hero": { amount: 1, type: "revive" },
"double-stage-coins": { amount: 1, type: "coin-2x" },
};
async function checkVerified(requestId, attempts = 4) {
for (let i = 0; i < attempts; i++) {
const res = await fetch(
`https://ads-verifier.verse8.io/ads/status?requestId=${encodeURIComponent(requestId)}`,
);
if (!res.ok && res.status !== 202) return false;
const body = await res.json();
if (body.status === "verified") return true;
if (body.status === "pending") {
await new Promise((r) => setTimeout(r, 1500));
continue;
}
return false; // dismissed | failed
}
return false;
}
async function claimAdReward({ account, requestId, placementId }) {
if (typeof requestId !== "string" || !requestId) throw new Error("missing_requestId");
const reward = REWARD_TABLE[placementId];
if (!reward) throw new Error("unknown_placement");
if (!(await checkVerified(requestId))) throw new Error("verification_failed");
// Atomic insert + credit. Unique constraint on (account, requestId) makes
// replays fail at the database layer; rollback prevents double-credit.
try {
await db.transaction(async (tx) => {
await tx.adGrants.insert({ account, requestId, placementId });
await tx.playerWallet.credit(account, reward.type, reward.amount);
});
} catch (err) {
if (isUniqueConstraintError(err)) throw new Error("already_granted");
throw err;
}
return { success: true };
}
```
Server rules:
- Match `placementId` against the table; refuse unknown placements.
- Grant amount comes from the server-side `REWARD_TABLE`, not from the client or the verifier response.
- Replay protection comes from a unique constraint on `(account, requestId)` inside the same transaction that credits the reward.
- Daily caps and other game-economy limits live in your normal player-state code; they're not part of `/ads/status` verification.
Last updated on