# SeaNet Roblox integration

## Files in this folder

| File | Put in Roblox as |
|------|-------------------|
| `SeaNetTracker.lua` | **ModuleScript** `SeaNetTracker` under **ServerScriptService** |
| `SeaNetAchievements.lua` | **ModuleScript** `SeaNetAchievements` next to `SeaNetTracker` |
| `SeaNetGameIntegration.server.lua` | **Script** under **ServerScriptService** (live demo + hooks for your game) |
| `SeaNetTest.server.lua` | **Script** under **ServerScriptService** (paste contents; name can be anything) |
| `SeaNetClientDemo.client.lua` | **LocalScript** under **StarterPlayer → StarterPlayerScripts** (only if demo remote is enabled on the server) |

## One-time setup

1. **Game Settings** → **Security** → enable **Allow HTTP requests**.
2. Open **SeaNetTracker** ModuleScript and set:
   - **`Config.ApiBase`** — your public SeaNet URL **with `https`**, **no trailing slash**  
     - Example: `https://your-app.vercel.app`  
     - Roblox servers **cannot** call `http://localhost` — use a deployed app or a tunnel (e.g. ngrok) to your dev machine.
   - **`Config.ApiKey`** — same plain string as `SEED_BRAND_API_KEY` in SeaNet `.env` (header `x-seanet-key`).
  - **`Config.BrandSlug`** *(required for creator keys)* — your creator/brand slug for scoped routes (example: `demo`).
3. Run **`npm run db:push`** then **`npm run db:seed`** on SeaNet so the **demo** brand exists and milestone slugs are defined: `first_quest`, `vip_pass`, `collector`, `defeated_boss_1`.
4. In creator dashboard open **`/dashboard/milestones`** and create every slug your game will send (achievement/game pass/promotion), including icon image if desired.
5. In **SeaNetAchievements**, edit **`ALLOWED_SLUGS`** for every slug you use.
6. **Profile:** unlocks are keyed by **Roblox user id**. The player must use **`/link`** on your SeaNet site so **`/profile`** shows milestones.

## Recommended: connect your game

1. Add **ModuleScripts** `SeaNetTracker` and `SeaNetAchievements` plus **Script** `SeaNetGameIntegration` (from the `.lua` files above) under **ServerScriptService**.
2. In Studio, add a **Part** in **Workspace**, rename it **`SeaNetAchievementGate`**, publish, and play: touching it should log `[SeaNet] unlockMilestone OK` and create `first_quest` for that Roblox user in SeaNet.
3. Replace that test with real calls: from your quest or progression **server** code, call `Achievements.grant(player, "first_quest", { questId = "intro" })` when the player truly completes the requirement (see comments at the bottom of `SeaNetGameIntegration.server.lua`).
4. Optional dev-only: set **`ENABLE_DEMO_REMOTE = true`** in `SeaNetGameIntegration.server.lua` and add **`SeaNetClientDemo.client.lua`** as a LocalScript to fire test unlocks — **do not ship** that pattern for real rewards.

## Module API (server-only)

```lua
local SeaNet = require(game.ServerScriptService.SeaNetTracker)

-- Analytics (profile funnel / dashboard)
SeaNet.track(player, "quest_completed", { questId = "intro" })

-- Profile: one achievement / pass (slug must exist in SeaNet for your brand)
SeaNet.unlockMilestone(player, "first_quest", { wave = 3 }) -- metadata optional

-- Profile: several slugs in one HTTP request
SeaNet.unlockMilestones(player, { "first_quest", "vip_pass" })
```

Whitelist wrapper (recommended from gameplay code):

```lua
local Achievements = require(game.ServerScriptService.SeaNetAchievements)
Achievements.grant(player, "first_quest", { wave = 3 })
Achievements.grantBatch(player, { "first_quest", "vip_pass" })
```

Call these from **Scripts / ModuleScripts on the server** only (after the player actually earned the reward).

## Place allowlist (optional)

If `Brand.allowedPlaceIds` is non-empty in the database, requests must send a matching `placeId`. The module sends `tostring(game.PlaceId)` by default. Override with `Config.PlaceId` if needed.

## Test script (`SeaNetTest`)

After `SeaNetTracker` is configured, add the **Script** from `SeaNetTest.server.lua`.

**In Play / Live:**

- Chat **`/seanet quest`** → unlock `first_quest`
- Chat **`/seanet pass`** → unlock `vip_pass`
- Chat **`/seanet all`** → batch unlock `first_quest`, `vip_pass`, `collector`

**Optional touch test:** add a **Part** named **`SeaNetTestPad`** to **Workspace**. When a character touches it, the server unlocks `first_quest`.

Check **Output** for `[SeaNet] unlockMilestone OK` or `warn` lines if HTTP/key/slug fails.

## HTTP reference (same as module)

**Events (shared route):** `POST {ApiBase}/api/v1/events`  
**Milestones (shared route):** `POST {ApiBase}/api/v1/milestones/unlock`  
**Events (brand-scoped):** `POST {ApiBase}/api/v1/brands/{BrandSlug}/events`  
**Milestones (brand-scoped):** `POST {ApiBase}/api/v1/brands/{BrandSlug}/milestones/unlock`  
Header: `x-seanet-key: <ApiKey>` (SeaNet does **not** read `Authorization: Bearer` or `x-api-key` for these routes.)

When `Config.BrandSlug` is set in `SeaNetTracker`, it automatically uses the brand-scoped routes. Creator keys are expected to use scoped routes.

Single unlock body:

```json
{
  "robloxUserId": "123456789",
  "slug": "first_quest",
  "placeId": "optional"
}
```

Batch:

```json
{
  "unlocks": [
    { "robloxUserId": "123456789", "slug": "first_quest" },
    { "robloxUserId": "123456789", "slug": "vip_pass" }
  ]
}
```

Unknown `slug` → **404**. Duplicate user + slug is **skipped** (idempotent).

## Troubleshooting: `Success=false` / `ConnectFail` in Output

Requests run on **Roblox’s servers**, not your computer. Studio is fine if **Game Settings → Security → Allow HTTP requests** is on.

- **`ApiBase` must be a real public `https` URL** (e.g. your Vercel app). Placeholders like `your-seanet-domain.example` will fail DNS (**ConnectFail**).
- **`http://localhost:3000` does not work** — Roblox cannot reach your machine’s loopback. Use a deployed SeaNet or a tunnel (ngrok, etc.) and put that URL in `Config.ApiBase`.
- **`HttpService.HttpEnabled` must be true.** Turn on **Game Settings → Security → Allow HTTP requests**. For an **unpublished** place in Studio, you may also need the Command bar: `game:GetService("HttpService").HttpEnabled = true`
- If **`Success=false`** but **`HttpError=nil`**, check the logged **`StatusCode`**, **`Body`**, and **`url=`** lines in Output — the module prints them. Often the cause is still a bad host, placeholder `ApiBase`, or HTTP disabled.

## Linking accounts

Profile UI reads unlocks by **Roblox user id**. The player must **link** that account on your SeaNet site (`/link`) for rows to show on **`/profile`**.
