# FlipKey Redeem SDK

A drop-in JS + CSS module for publisher redemption pages. Handles wallet-connect, signature, and per-platform handoff for Steam, Xbox, PlayStation, Nintendo Switch, and Epic Games.

**FlipKey itself never holds or transits your keys.** This SDK is reference code for *your* redemption page on *your* domain. Your backend retrieves the key from your own infrastructure when a redemption request arrives, and delivers it to the buyer's browser through a controlled response — never via HTML attributes or query strings.

## Quick start

```html
<link rel="stylesheet" href="https://flipkey.gg/sdk/redeem/flipkey-redeem.css">
<div class="fk-redeem"
     data-platform="steam"
     data-token-id="42"
     data-redeem-endpoint="/api/redeem"
     data-publisher="Acme Games"></div>
<script src="https://flipkey.gg/sdk/redeem/flipkey-redeem.js"></script>
<script>FlipKeyRedeem.mountAll();</script>
```

The widget renders a "Connect Wallet & Redeem" button. On click it:
1. Connects the user's browser wallet (`window.ethereum`).
2. Asks the wallet to sign a domain-bound message including the token ID and a fresh nonce.
3. POSTs `{ tokenId, wallet, signature, nonce, issuedAt }` to `data-redeem-endpoint`.
4. Renders the response: a redirect (Steam, PSN), an entitlement-granted notice (direct platform-API delivery), or a key view that hides the key after 5 minutes (Xbox, Nintendo, Epic, in-house — paste flow). Hidden is not lost: the same wallet can sign again and your backend should return the same key (the Publisher Starter kit 0.3.4+ does).

## Required attributes

| Attribute | Value |
|---|---|
| `data-platform` | One of `steam`, `xbox`, `playstation`, `nintendo`, `epic`, `inhouse` |
| `data-token-id` | Numeric ID of the FlipKey NFT being redeemed (from the URL or your route) |
| `data-redeem-endpoint` | Your backend handler URL (POST). The SDK never calls FlipKey directly |

`data-publisher` is optional and is rendered in the widget heading.

## Backend handler contract

Your `data-redeem-endpoint` receives:

```json
{
  "tokenId": "42",
  "wallet": "0xabc...",
  "signature": "0x...",
  "nonce": "deadbeef...",
  "issuedAt": 1730000000000
}
```

Your handler must:

1. Verify `signature` was produced by `wallet` over the message format the SDK uses (see `examples/redeem-handler.js` for the canonical format).
2. Verify `nonce` is fresh (not seen before, within ~5 minutes of `issuedAt`) — replay protection.
3. Verify on-chain that `wallet` owns `tokenId` in your FlipKey-deployed contract.
4. Atomically claim a key from your inventory (`UPDATE keys SET status='redeemed' WHERE status='unredeemed' LIMIT 1`).
5. Report the activation back to FlipKey, signed with your `webhook_secret`.
6. Return one of:

```json
{ "result": "redirect", "url": "https://store.steampowered.com/account/registerkey?key=..." }
{ "result": "entitlement_granted" }
{ "result": "show_key", "key": "XXXXX-XXXXX-XXXXX", "redeemUrl": "https://...", "expiresInSec": 90 }
```

Use `redirect` for platforms that accept pre-fill (Steam, PSN). Use `entitlement_granted` if you used a direct platform API to grant the game without exposing a key (Steamworks `GrantPackage`, etc.) — preferred when available. Use `show_key` only for platforms that don't support pre-fill (Xbox, Nintendo, Epic).

A complete reference handler is in [`examples/redeem-handler.js`](examples/redeem-handler.js).

## Link-test handler

Before FlipKey will let you list a game, every redemption URL on the listing must pass a live link test. FlipKey calls your URL with `?fk_test=1&nonce=<random>` and expects:

```json
{ "nonce": "<echoed>", "sig": "<hex hmac_sha256(your_webhook_secret, \"fk_link_test:\" + nonce)>" }
```

This proves the URL exists, runs your handler, and that you control the FlipKey-issued `webhook_secret`.

A drop-in implementation is in [`examples/link-test-handler.js`](examples/link-test-handler.js). Mount it as middleware in front of your real redemption page handler — it short-circuits when `fk_test=1` is present and falls through otherwise.

## Per-platform notes

| Platform | Delivery | Code format | Notes |
|---|---|---|---|
| `steam` | Redirect (pre-fill) | `XXXXX-XXXXX-XXXXX` | Steam's `?key=` accepts the code; cleanest UX. Direct entitlement also possible via Steamworks `GrantPackage` |
| `xbox` | Show-key (paste) | 25-char with hyphens | Microsoft's `redeem.microsoft.com` doesn't accept pre-fill |
| `playstation` | Redirect (pre-fill) | 12-digit (often `XXXX-XXXX-XXXX`) | Sony's `?voucherCode=` opens the redemption modal pre-filled |
| `nintendo` | Show-key (paste) | 16-digit | `ec.nintendo.com/redeem`; account must have accessed eShop on a Switch at least once |
| `epic` | Show-key (paste) | varies | `epicgames.com/store/redeem`; Epic's pre-fill OAuth is a separate (gated) integration |

## Security model — read this carefully

The SDK enforces these properties; your backend handler is responsible for the rest.

### What the SDK guarantees

- **The key is never written to a `data-*` attribute, `value` attribute, `aria-label`, or any other HTML attribute.** Browser extensions, third-party scripts (analytics, ads, support widgets, session-replay), and corporate proxies that snapshot DOM state cannot scrape what isn't there.
- **The key is never written to `localStorage`, `sessionStorage`, or cookies.**
- **The key, when shown (paste-flow platforms only), lives in a single closure variable, set into the DOM with `.textContent`, and is hidden after 5 minutes or on navigate-away.** (0.8.2 no longer wipes it when the tab goes hidden — alt-tabbing to paste the key is the normal flow. A hidden key is re-shown by signing again with the same wallet; see the changelog.)
- **The key never appears in a URL or query string.** The redirect URL the SDK navigates to is built by *your* backend and returned in a JSON response over HTTPS.

### What you (the publisher) are responsible for

- **Don't render keys server-side into HTML.** Even with HTTPS, server-rendered keys land in the DOM, get scraped by extensions and session-replay tools, and leak into logging / monitoring services that snapshot the page. Use the SDK's flow instead — keys arrive in a JSON response to an authenticated POST and stay in JS memory.
- **Set `Cache-Control: no-store` on every response that may contain key material** — both your `/api/redeem` JSON response and the redemption page HTML. CDN caches, corporate proxies, and browser caches will otherwise persist key material. The reference handler in `examples/redeem-handler.js` sets this header at the top of the route; do not remove it.
- **Prefer direct entitlement over key delivery.** Steam, PSN, Xbox, and Nintendo all support backend-to-backend entitlement grants for games owned/published by you. When you use these, no key string ever exists in the wild. Return `{ result: "entitlement_granted" }` and your buyer never sees a code at all.
- **Atomic key claiming.** `UPDATE keys SET status='redeemed' WHERE status='unredeemed' LIMIT 1` in a transaction. Two concurrent redemption requests must never get the same key.
- **Replay protection.** Track used nonces (Redis with TTL, or a `nonces` table). A signature is reusable forever otherwise.
- **HTTPS everywhere.** Both your redemption page and your `data-redeem-endpoint`.
- **Don't log the key.** Strip the `key` field from request/response logging in your APM, and from any error reporters (Sentry, Bugsnag, etc.). These services often capture full responses by default.
- **Rate-limit by wallet + IP.** A signature flood is not a real redemption.

### What FlipKey does NOT see or store

- The keys themselves (we never have them).
- The contents of the user's wallet beyond the public address.
- The signed message contents beyond what the activation report includes (token ID, wallet, platform, timestamp).

## Programmatic API

```js
FlipKeyRedeem.mount(document.querySelector('#my-redeem-widget'));
FlipKeyRedeem.mountAll('.fk-redeem'); // selector optional, defaults to '.fk-redeem'
FlipKeyRedeem.PLATFORMS;              // platform metadata
FlipKeyRedeem.version;                // current SDK version
```

## Migrating from v0.1.x

v0.1.x took the key as `data-key` on the widget element and rendered it directly. **That pattern is unsafe and has been removed in v0.2.0.** If you are on v0.1.x, you must:

1. Update the widget HTML to remove `data-key` and add `data-token-id` + `data-redeem-endpoint`.
2. Implement a backend handler at the `data-redeem-endpoint` URL that follows the contract above.
3. Implement the link-test handler so your redemption URL can pass FlipKey's listing-time verification.
4. Remove any server-side template logic that injects the key into the page HTML.

The reference handlers in `examples/` are the fastest path; for most publishers it's a one-day port.
