From 7b5c911be6f07905fd42b231dca81ed706597b4e Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Tue, 15 Sep 2026 18:05:29 -0700 Subject: [PATCH] oskiewar steam: the marketing model, a field-ordered fill sheet, and a wishlist puller MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MARKETING.md says what Steam rewards (purchases and playtime, never page traffic or wishlists), so the plan is three loops — coming-soon wishlist accumulation off the reel factory, the launch email, update rounds. Valve's partner Web API has nothing for the page or events; it has exactly two marketing reads, GetAppWishlistReporting and GetDetailedSales, and bin/wishlists.mjs pulls both into the vault. store-page/fill.md walks the partner editor field by field with the value or file for each. --- xbox/steam/MARKETING.md | 95 +++++++++++++++++++++++++++++++++++ xbox/steam/STEAM.md | 8 +-- xbox/steam/bin/wishlists.mjs | 90 +++++++++++++++++++++++++++++++++ xbox/steam/store-page/fill.md | 80 +++++++++++++++++++++++++++++ 4 files changed, 270 insertions(+), 3 deletions(-) create mode 100644 xbox/steam/MARKETING.md create mode 100755 xbox/steam/bin/wishlists.mjs create mode 100644 xbox/steam/store-page/fill.md diff --git a/xbox/steam/MARKETING.md b/xbox/steam/MARKETING.md new file mode 100644 index 0000000000..f9510405ca --- /dev/null +++ b/xbox/steam/MARKETING.md @@ -0,0 +1,95 @@ +# oskiewar on Steam — the marketing model + +Written 2026-09-15, the day the appid (5280790) arrived. The reel factory's +own manual is `xbox/live/MARKETING.md`; this file is the Steam-shaped half: +what Steam counts, what it ignores, which of those numbers a script can pull, +and which surfaces only a browser can touch. + +## What Steam actually rewards + +From Valve's visibility doc (sources in `STEAM.md`): + +- **Launch visibility is automatic and unpurchasable.** Every new release + goes into the New Releases Queue, weighted toward titles with the *fewest* + views. New & Trending follows if the title "is doing well" — purchases and + playtime, not page traffic. +- **Store page traffic and conversion rate are explicitly not factors.** + Neither are wishlists, algorithmically. +- **Wishlists are a mailing list.** Every wishlister gets an email at launch, + at any discount of 20%+ (8-hour minimum), and when a demo ships. A two-week + cooldown per app. That is their whole value, and it is a large one: the + launch email is the only marketing Steam sends on the game's behalf. +- **Tags and language support** are the two page-side levers that change who + the recommender shows the game to. Wrong tags buy the wrong audience. +- **Update visibility rounds** are unlimited after the launch window — each + substantive update earns a "Recently Updated" placement. + +So the model has three loops, in order of when they run: + +1. **Coming-soon → wishlist accumulation.** Outside-Steam traffic (the reel + factory, three reels a day, already captioned) lands on the coming-soon + page; the page converts to wishlists; nothing else matters until launch. + Two levers: the reel captions carry the store URL, and the page itself is + honest, tagged right, and has a trailer. +2. **Launch → the email + the queue.** Release day converts the wishlist + list; the queue does its own thing. Lever: pick the day, ship the build + that the page describes, and post the launch event (the one canvas where + marketing text is legal). +3. **Updates → visibility rounds.** Every version bump that changes play is + an update round with an event post. The game ships a version a day on + the web; Steam updates should be batched into rounds with something to + say. + +## What a script can pull + +Valve's partner Web API has **nothing** for editing a store page, posting +events, or reading store traffic — those are the partner site, a browser. +It has exactly two marketing-relevant read endpoints, both on +`IPartnerFinancialsService` and both needing a publisher key with the +Financial permission: + +| Endpoint | Grain | Returns | +|---|---|---| +| `GetAppWishlistReporting` | app × GMT day | adds / deletes / purchases, per country, per language | +| `GetDetailedSales` | Pacific day, paged by `highwatermark_id` | every sale row with package, country, discount | + +`bin/wishlists.mjs` pulls both into `vault/oskiewar/steam/*.jsonl`. The key +lives in `vault/oskiewar/steam.env` (`STEAM_PUBLISHER_KEY`), minted at +partner.steamgames.com → Users & Permissions → Manage Groups → Create WebAPI +Key, and IP-allowlisted if the pull runs from one box. + +**Attribution** is a join, not an API: the reel ledger +(`xbox/live/marketing/ledger.json`, one row per posted reel with day, slot, +segment, and Meta insights) against the wishlist series by day. A reel that +moves wishlists shows up as a day with adds above the segment's baseline; +a segment that never does is a caption problem. That join is the first +report worth writing once the page has been public two weeks — before that +there is no baseline. + +## What only a browser can do + +| Task | Route | +|---|---| +| Fill and edit the store page (`store-page/fill.md`) | Claude Chrome extension driving partner.steamgames.com; the fill sheet is field-ordered for exactly this | +| Store traffic breakdown, wishlist conversion by email | partner site → Marketing & Visibility → Traffic Breakdown (no API) | +| Post events / announcements (launch, update rounds) | partner site event editor | +| Mark ready for review; set live; press Release | human — each is a deliberate button | +| Set a build live on a branch | partner site SteamPipe page (`steamcmd` can set `SetLive` on upload, but only to branches without a password) | + +The extension is the CDP route this repo trusts for a signed-in browser: +the puppet daemon's own CDP path to neo failed on 2026-09-15 and a +hand-rolled CDP script against the user's Chrome is (correctly) refused by +the harness. Keep automation on the extension, keep the fill sheet the source +of truth, and let the human press the three buttons. + +## The calendar it implies + +- **Now → page approved (3–5 business days):** fill the page from + `store-page/fill.md`; upload the depots; cut a trailer from the 16:9 lane. +- **Coming-soon live (≥14 days):** reel captions point at the store URL; + `wishlists.mjs` runs daily; watch the country split — it decides whether + the launch-day discount is worth a regional price pass. +- **Launch (≥ 2026-10-01):** release button, launch event with the 800×450 + cover, then nothing for two weeks while the queue runs. +- **After:** update rounds on real changes, a 20%+ discount no sooner than + the email cooldown allows, and the reel factory unchanged. diff --git a/xbox/steam/STEAM.md b/xbox/steam/STEAM.md index f02d613d00..4b9653df58 100644 --- a/xbox/steam/STEAM.md +++ b/xbox/steam/STEAM.md @@ -156,11 +156,13 @@ directory. What remains is one human gate and the buttons behind it. | Trailer | **Not made.** Required in practice; the 16:9 lane is one parameter away from the reel factory | | Shell | **Packaged and booting** on macOS with steamworks.js wired; Linux and Windows cross-built (`shell/README.md`) | | Depots | `depots/depots.mjs --appid=` + `depots/upload.sh` (steamcmd installed via Homebrew) | -| Appid | **Blocked on identity review** — filed 2026-09-01, priced at up to 10 business days, which is now. Check the dashboard | +| Appid | **5280790**, assigned 2026-09-15 — the identity review cleared. Depots 5280791 (win), 5280792 (mac), 5280793 (linux). Shell rebuilt against it; `depots/out/` written | +| Build upload | `STEAM_BUILD_USER=jeffreyscudder sh depots/upload.sh` — interactive (password + Steam Guard), so a human runs it | +| Store page fill | `store-page/fill.md` is field-ordered for the editor; driven through the Claude Chrome extension once it is un-paused | +| Marketing model | `MARKETING.md`; `bin/wishlists.mjs` pulls wishlist/sales days once a publisher key exists | | Build review | Cannot start until the store page is approved | -**The order from here.** (1) Dashboard: is the KYC item cleared? If so, -`Complete Account Creation` unlocks — name the app, take the appid. +**The order from here.** (1) Done — the app is claimed, appid 5280790. (2) Fill the store page from `store-page/` and upload `store-page/assets/`; run the content survey; set the price; mark ready for review. (3) Coming-soon page goes public on approval — that is the wishlist diff --git a/xbox/steam/bin/wishlists.mjs b/xbox/steam/bin/wishlists.mjs new file mode 100755 index 0000000000..46145b5155 --- /dev/null +++ b/xbox/steam/bin/wishlists.mjs @@ -0,0 +1,90 @@ +#!/usr/bin/env node +// Pull oskiewar's wishlist reporting out of Steamworks, one day at a time. +// +// node xbox/steam/bin/wishlists.mjs # every day since the page went public +// node xbox/steam/bin/wishlists.mjs --since=2026-09-20 --until=2026-09-27 +// node xbox/steam/bin/wishlists.mjs --sales # GetDetailedSales instead +// +// Valve exposes exactly two partner endpoints that speak to marketing — +// IPartnerFinancialsService/GetAppWishlistReporting (adds, deletes, +// purchases, per country and language, per GMT day) and GetDetailedSales +// (per Pacific day) — and nothing for the store page itself, which stays a +// browser job. Both need a publisher Web API key with the Financial +// permission, minted at partner.steamgames.com → Users & Permissions → +// Manage Groups → Create WebAPI Key, and kept out of the tree: +// +// vault/oskiewar/steam.env: STEAM_PUBLISHER_KEY=... +// +// Days land as JSON lines in vault/oskiewar/steam/wishlists.jsonl (sales in +// sales.jsonl), one record per day, re-pulled when asked again — Valve +// revises recent days. The reel factory's ledger (xbox/live/marketing/ +// ledger.json) is the other half of the picture: MARKETING.md explains how +// the two are read together. + +import { appendFile, mkdir, readFile } from "node:fs/promises"; +import { resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const here = resolve(fileURLToPath(new URL(".", import.meta.url))); +const repo = resolve(here, "../../.."); +const appid = Number((await readFile(resolve(here, "../shell/steam_appid.txt"), "utf8")).trim()); +const flags = new Map(process.argv.slice(2).map((entry) => { + const [key, value = "true"] = entry.replace(/^--/, "").split("="); + return [key, value]; +})); + +const env = Object.fromEntries((await readFile(resolve(repo, "vault/oskiewar/steam.env"), "utf8") + .catch(() => { throw new Error("vault/oskiewar/steam.env is missing — mint a publisher key first"); })) + .split("\n").filter((line) => line.includes("=") && !line.startsWith("#")) + .map((line) => line.split("=").map((part) => part.trim()))); +const key = env.STEAM_PUBLISHER_KEY; +if (!key) throw new Error("STEAM_PUBLISHER_KEY is not set in vault/oskiewar/steam.env"); + +const host = "https://partner.steam-api.com"; +const store = resolve(repo, "vault/oskiewar/steam"); +await mkdir(store, { recursive: true }); + +const day = (date) => date.toISOString().slice(0, 10); +const until = flags.get("until") ? new Date(flags.get("until")) : new Date(Date.now() - 864e5); +// The coming-soon page is the earliest date with anything to count. +const since = new Date(flags.get("since") || "2026-09-16"); + +async function call(method, params) { + const url = new URL(`${host}/IPartnerFinancialsService/${method}/v001/`); + url.search = new URLSearchParams({ key, ...params }); + const response = await fetch(url); + if (!response.ok) throw new Error(`${method} ${response.status}: ${await response.text()}`); + return (await response.json()).response; +} + +if (flags.has("sales")) { + for (let at = new Date(since); at <= until; at.setUTCDate(at.getUTCDate() + 1)) { + const date = day(at); + const rows = []; + let highwatermark_id = 0; + for (;;) { + const page = await call("GetDetailedSales", { date, highwatermark_id }); + rows.push(...(page.results || [])); + if (!page.max_id || Number(page.max_id) === highwatermark_id) break; + highwatermark_id = Number(page.max_id); + } + await appendFile(resolve(store, "sales.jsonl"), + JSON.stringify({ date, pulled: new Date().toISOString(), rows }) + "\n"); + console.log(`${date} ${rows.length} sale rows`); + } +} else { + console.log("date adds dels buys top countries"); + for (let at = new Date(since); at <= until; at.setUTCDate(at.getUTCDate() + 1)) { + const date = day(at); + const report = await call("GetAppWishlistReporting", { appid, date }); + await appendFile(resolve(store, "wishlists.jsonl"), + JSON.stringify({ date, pulled: new Date().toISOString(), ...report }) + "\n"); + const s = report.wishlist_summary || {}; + const top = (report.country_summary || []) + .sort((a, b) => (b.adds ?? b.wishlist_adds ?? 0) - (a.adds ?? a.wishlist_adds ?? 0)) + .slice(0, 3).map((c) => c.country_code ?? c.country).join(" "); + console.log(`${date} ${String(s.adds ?? s.wishlist_adds ?? "?").padStart(4)} ` + + `${String(s.deletes ?? s.wishlist_deletes ?? "?").padStart(4)} ` + + `${String(s.purchases ?? s.wishlist_purchases ?? "?").padStart(4)} ${top}`); + } +} diff --git a/xbox/steam/store-page/fill.md b/xbox/steam/store-page/fill.md new file mode 100644 index 0000000000..5a13fc68c7 --- /dev/null +++ b/xbox/steam/store-page/fill.md @@ -0,0 +1,80 @@ +# oskiewar — store page fill sheet (appid 5280790) + +One row per partner-site field, in the order the editor walks them, with the +exact value to paste or the file to upload. `copy.md` and `metadata.md` are +the reasoning; this is the keyboard. Anything marked *human* is a decision, +not a paste. + +Editor: https://partner.steamgames.com/apps/landing/5280790 → **Edit Store Page**. + +## Basic Info + +| Field | Value | +|---|---| +| Application name | `oskiewar` | +| Developer | `Jeffrey Alan Scudder` *(human: or "Aesthetic Computer" — public on the page)* | +| Publisher | same as developer | +| Franchise | leave blank | +| Release date | *human* — "Coming soon" now; earliest 2026-10-01 | +| Supported languages | English — Interface ✓ · Full Audio ✓ · Subtitles ✗ | +| Genre (primary) | Action | +| Genre (secondary) | Indie | +| Tags (first five weigh) | Fighting · 2D Fighter · Local Multiplayer · Physics · Arcade · PvP · Multiplayer · Singleplayer · Action · Indie · Minimalist · Funny · Retro · Controller · Competitive | +| Categories | Single-player ✓ · Multi-player ✓ · PvP ✓ · Shared/Split Screen PvP ✓ · Full Controller Support — *only after the pad-only pass* · Remote Play Together — *only after a two-network test* | +| Controller support | Full (Xbox) — same caveat | +| Accessibility | *human* — no text entry, no chat; nothing to declare yet | +| Anti-cheat | None | +| Social links | https://oskiewar.com · https://instagram.com/whistlegraph *(human: which ones)* | +| Support | mail@aesthetic.computer · https://oskiewar.com/support.html | +| Legal line | `© 2026 Jeffrey Alan Scudder` | + +## Description + +| Field | Value | +|---|---| +| Short description (≤300) | *copy.md → Short description* (294 chars) | +| About this game | *copy.md → Long description*, headers in bold, no images at first pass | +| Mature content description | leave blank; content survey covers it | +| System requirements — Windows | Min: Windows 10 64-bit · dual-core · 4 GB RAM · any GPU with hardware acceleration · 500 MB. Additional notes: `The game is 264 KB. The other 499 MB is the browser it ships inside.` | +| System requirements — macOS | Min: macOS 11 · Apple silicon or 64-bit Intel · 4 GB RAM · 500 MB | +| System requirements — Linux + SteamOS | Min: Ubuntu 22.04 / SteamOS 3.x · dual-core · 4 GB RAM · 500 MB | + +## Graphical Assets (upload from `assets/`) + +| Slot | File | +|---|---| +| Header capsule 920×430 | `header-capsule.png` | +| Small capsule 462×174 | `small-capsule.png` | +| Main capsule 1232×706 | `main-capsule.png` | +| Vertical capsule 748×896 | `vertical-capsule.png` | +| Screenshots (5+) | `screenshot-1.png` … `screenshot-5.png` — tick all as "suitable for all ages" | +| Page background 1438×810 | `page-background.png` | +| Library capsule 600×900 | `library-capsule.png` | +| Library header 920×430 | `library-header.png` | +| Library hero 3840×1240 | `library-hero.png` | +| Library logo | `library-logo.png` (1280×342, transparent) — position: centre-bottom | +| Client icon 256×256 | `client-icon.png` | +| Community icon 184×184 | `community-icon.jpg` | + +## Trailers + +None yet. The page can be submitted without one; Valve's review treats a +missing trailer as a weakness, not a block. Do not press Release while one +is still encoding. + +## Store Settings / Content survey + +Cartoon violence between stick figures; no blood or gore; no text chat, no +user-generated content, no gambling, no nudity, no mature themes. Answer +from the Steam build, which has the community surfaces compiled out. + +## Pricing + +*human.* Base USD price with Valve's suggested per-currency conversions. + +## Publish + +Order: save every tab → **Mark as ready for review** on the store page → +3–5 business days → coming-soon goes public (that is the wishlist button) → +build review → release on or after the later of 2026-10-01 and two weeks of +coming-soon. -- 2.51.2