From cdabe7338b19d250ac47d7cf4480669937523f1d Mon Sep 17 00:00:00 2001 From: Andrew Pyanzin Date: Fri, 14 Aug 2026 19:46:13 +0300 Subject: [PATCH 1/2] feat: add game-web-portal skill MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agentic onboarding for a PC/Steam Game Web Portal: the agent creates or resumes a verified portal and returns an evidence-backed partner handoff. Nothing is reported as completed until its effect is verified. - skills/game-web-portal/SKILL.md — the state flow, entry conditions, and per-step contract - references/agentic-onboarding.md — acceptance scenarios, the per-state evidence contract, and the handoff template - references/portal-api.md — the Shop Builder API the portal steps call: endpoints per stage, the domain vs landing _id split, batch-patch and localization payload shapes, known API issues, response -> status mapping - registry rows in skills/README.md and the AGENTS.md/CLAUDE.md inventory Supersedes #25, which delegated the portal work to a skill that has no Shop Builder coverage. --- AGENTS.md | 4 + CLAUDE.md | 4 + skills/README.md | 1 + skills/game-web-portal/SKILL.md | 150 +++++++++ .../references/agentic-onboarding.md | 187 +++++++++++ .../game-web-portal/references/portal-api.md | 293 ++++++++++++++++++ 6 files changed, 639 insertions(+) create mode 100644 skills/game-web-portal/SKILL.md create mode 100644 skills/game-web-portal/references/agentic-onboarding.md create mode 100644 skills/game-web-portal/references/portal-api.md diff --git a/AGENTS.md b/AGENTS.md index 090c69c..d06c035 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,6 +17,7 @@ Skills call **Xsolla REST APIs directly**. The CLI (`xsolla/xsolla-cli`) is an o | Skill | What it does | |---------------------------------|------------------------------------------------------------------------------------------| | `shop-setup` | **Orchestrator** — coordinates the full zero-to-shop flow, chaining all domain skills | +| `game-web-portal` | **Orchestrator** — PC/Steam Game Web Portal: create or resume a portal, verified handoff | | `merchant-setup` | Creates and configures an Xsolla account + get API key | | `catalog-design` | Configures the catalog and the client flow: client catalog, purchase, order confirmation | | `login-setup` | Integrates Xsolla Login / NewID authentication | @@ -41,6 +42,9 @@ Configure my Xsolla catalog with items and pricing Integrate payments into my game → triggers: headless-checkout-integration +Set up my PC Game Portal +→ triggers: game-web-portal + Go live / leave sandbox → triggers: production ``` diff --git a/CLAUDE.md b/CLAUDE.md index 090c69c..d06c035 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,6 +17,7 @@ Skills call **Xsolla REST APIs directly**. The CLI (`xsolla/xsolla-cli`) is an o | Skill | What it does | |---------------------------------|------------------------------------------------------------------------------------------| | `shop-setup` | **Orchestrator** — coordinates the full zero-to-shop flow, chaining all domain skills | +| `game-web-portal` | **Orchestrator** — PC/Steam Game Web Portal: create or resume a portal, verified handoff | | `merchant-setup` | Creates and configures an Xsolla account + get API key | | `catalog-design` | Configures the catalog and the client flow: client catalog, purchase, order confirmation | | `login-setup` | Integrates Xsolla Login / NewID authentication | @@ -41,6 +42,9 @@ Configure my Xsolla catalog with items and pricing Integrate payments into my game → triggers: headless-checkout-integration +Set up my PC Game Portal +→ triggers: game-web-portal + Go live / leave sandbox → triggers: production ``` diff --git a/skills/README.md b/skills/README.md index d481da9..3344f56 100644 --- a/skills/README.md +++ b/skills/README.md @@ -7,6 +7,7 @@ Each subdirectory contains a `SKILL.md` — a structured workflow file for an Xs | Skill | Domain | Owner | Status | |---------------------------------------------------------------------------|---------------------------------------|---------------------|--------| | [`shop-setup`](shop-setup/SKILL.md) | Orchestrator — full zero-to-shop flow | @y.klochikhin | Done | +| [`game-web-portal`](game-web-portal/SKILL.md) | Orchestrator — PC/Steam Game Portal | @a.pyanzin | Draft | | [`merchant-setup`](merchant-setup/SKILL.md) | Merchant and Project setup | @y.klochikhin | Done | | [`catalog-design`](catalog-design/SKILL.md) | Items, purchase & order tracking | @p.sanachev | Draft | | [`login-setup`](login-setup/SKILL.md) | Login / NewID / auth | @mohammed_abujalala | Draft | diff --git a/skills/game-web-portal/SKILL.md b/skills/game-web-portal/SKILL.md new file mode 100644 index 0000000..12f0a56 --- /dev/null +++ b/skills/game-web-portal/SKILL.md @@ -0,0 +1,150 @@ +--- +name: game-web-portal +description: >- + Set up, create, configure, or resume an Xsolla Game Web Portal for a PC or Steam + game. Use when onboarding a PC or Steam title, creating or resuming a Game Portal, + wiring a Web Shop, Launcher, or Login into a portal, generating a preview link, or + checking readiness and publication status. Covers the Home, News, Rewards, Web Shop, + Community, and optional Launcher sections plus catalog, theme, localization, preview, + and publication gates. Invoke for "set up my PC Game Portal", "resume my portal + without duplicating pages", "give me a verified preview", or "is my portal ready to + publish". PC and Steam only — App Store and Google Play onboarding is out of scope + and returns needs_input. +metadata: + owner: a.pyanzin + domain: orchestrator + status: draft +--- + +# Game Web Portal + +Create or resume a verified PC/Steam Game Web Portal and return an evidence-backed +partner handoff. Honest partial completion is correct; simulated completion is failure. + +## When to use + +Trigger keywords: PC Game Portal, Game Web Portal, Steam onboarding, portal resume, +Web Shop, Launcher, Login binding, preview link, readiness, publication. + +Entry conditions: + +- The title ships on PC and the store URL host is exactly `store.steampowered.com`. + Mobile, unknown, invalid, or spoofed hosts return `needs_input`. +- A publisher context is authenticated and a merchant/project pair is confirmed. +- The caller has decided whether an existing portal at the domain should be updated + or a new one created. + +Use `shop-setup` instead for a general zero-to-shop storefront; this skill is the +PC/Steam portal path and delegates catalog, Login, and checkout work to the skills +listed under Steps. + +## Prerequisites + +- Authenticated publisher context. +- Confirmed merchant ID and project ID — see `merchant-setup`. +- API key for catalog and payment operations where required. +- Shop Builder access for the target project. Portal operations authenticate with a + Publisher Account admin token, not the project API key — see + [references/portal-api.md](references/portal-api.md). Treat `401/403` as + `needs_access`, never as a capability block. +- Approved content and brand assets. Never invent or reuse partner identifiers, + credentials, content, prices, assets, or URLs. +- Required input, collected in a single question rather than one at a time: + +```yaml +merchant_id: +project_id: +domain: +game_name: +store_url: +primary_locale: +existing_portal_policy: update | create-new +``` + +Optional: approved logo, hero, screenshots, colors and fonts; an existing Store +catalog; additional locales; analytics IDs; an existing Launcher and build. + +## Steps + +The run is a state flow. Every mutation is preceded by an existing-vs-desired-state +check and followed by read-back and ledger update. + +```text +Intake → Preflight → Discover → Draft → Verify → Human review → +Publish → Live verification → Handoff +``` + +1. **Intake** — collect the required input above. Resolve every ambiguity by asking; + never choose an ambiguous match. +2. **Preflight** — validate the exact Steam host, read the title through + `GET .../landing/{domain}/parsing` with `type: steam`, confirm merchant/project, + domain, and locale, and disclose any unsupported or human-gated step up front. + Never substitute invented game metadata when parsing fails. +3. **Discover** — list existing portals and read the target structure + (`GET .../landings`, `GET .../landing/{domain}`, `GET .../landing/{domain}/structure`). + Capture the landing `_id`, page IDs, and block IDs before any mutation — block and + theme calls are keyed by landing `_id`, not the domain. Never recreate a discovered + existing entity; resume at the first incomplete item. +4. **Draft** — bootstrap first: generate the portal from the store URL + (`POST .../landing/{domain}/structure` with `type: steam`, or + `POST .../landing/{domain}/portal` on a landing with no type yet — it returns + `409` once initialized, which means resume, not recreate). Then apply one change + group at a time across Home, News, Rewards, Web Shop, Community, and optional + Launcher. Portal structure itself — pages, blocks, theme, assets, copy and + localization, domain, analytics, preview — runs against the Shop Builder API in + [references/portal-api.md](references/portal-api.md). Delegate the surrounding + products rather than duplicating their recipes: `merchant-setup` for + merchant/project/API key, `catalog-design` for catalog and pricing, `login-setup` + for Login, `headless-checkout-integration` for checkout. Placeholders require + approval and a visible label. +5. **Verify** — read back every changed entity (`structure`, page, and block reads), + refresh the preview, run the readiness check (`GET .../landing/{domain}/check`), + confirm rendered output, and update the ledger. A mutation response is not + evidence. Keep unverified items out of **Completed**. +6. **Human review** — present completed items, placeholders, blockers, and failures. + `draft_ready` requires correct ownership, domain, type, and locale, no duplicate + routes, verified content, disclosed placeholders, explicit Login and commerce + status, and no readiness failure. +7. **Publish** — require explicit approval, then `POST .../landing/{domain}/publication` + with the selected page IDs: publication is per-page, the main page must be live or + in the same selection, no section may be empty, and the licensing agreement must be + signed. Never publish automatically, and never treat the `200` as proof the live + site is correct — it is a receipt, and `last_published` still has to be confirmed + against the public URL in Step 8. Rollback is `GET .../landing/{domain}/versions` + plus `PUT .../landing/{domain}/versions/{versionId}`. +8. **Live verification** — `published_verified` requires the expected public version + and routes, working Login and account binding, and verified Web Shop and Launcher + outcomes. +9. **Handoff** — repeat merchant ID, project ID, domain, Steam URL, and locale, with + evidence for every completed item. + +Status values: `completed`, `placeholder`, `needs_input`, `needs_access`, +`needs_human`, `blocked_capability`, `failed`. + +Two references, both loaded before issuing changes: + +- [references/agentic-onboarding.md](references/agentic-onboarding.md) — the full + specification: `GIVEN / WHEN / THEN` acceptance scenarios, the per-state evidence + contract, and the handoff report template. +- [references/portal-api.md](references/portal-api.md) — the Shop Builder API for + Steps 2–8: endpoints per stage, the domain vs landing `_id` split, the localization + payload shape, known API issues, and the response → status mapping. + +## Common pitfalls + +1. **Duplicate pages and blocks on resume.** Onboarding an existing domain without + reading the current structure first creates a second Home or Web Shop. Always + discover and compare existing versus desired state before any mutation. +2. **HTTP 200 read as published.** A live URL can serve an older version. Publication + stays incomplete until the expected version and routes are confirmed live. +3. **Login sign-in mistaken for Login done.** Sign-in can succeed while account + binding fails. Both Login and onboarding remain incomplete until binding is + verified. +4. **Launcher marked complete without a verified download.** A Launcher needs a real + Launcher on the project, an uploaded build, a generated installer, and a verified + installer download. Refuse publish-anyway when that evidence is missing. +5. **Access expiry losing progress.** On `401/403` mid-run, preserve the ledger and + return `needs_access`, then reauthenticate, re-read state, and resume — do not + restart the portal. +6. **Reporting completion without read-back.** Nothing enters **Completed** while + read-back, rendered output, or end-to-end verification is pending. diff --git a/skills/game-web-portal/references/agentic-onboarding.md b/skills/game-web-portal/references/agentic-onboarding.md new file mode 100644 index 0000000..218d1fe --- /dev/null +++ b/skills/game-web-portal/references/agentic-onboarding.md @@ -0,0 +1,187 @@ +# Xsolla Game Web Portal — Agentic Onboarding Specification + +## Requirements + +### Input + +Collect missing values in one question: + +```yaml +merchant_id: +project_id: +domain: +game_name: +store_url: +primary_locale: +existing_portal_policy: update | create-new +``` + +Optional: approved brand assets, analytics IDs, and locales. Never invent or +reuse partner identifiers, credentials, content, prices, assets, or URLs. + +### Acceptance scenarios + +1. **Platform:** GIVEN a store URL, WHEN the exact host is + `store.steampowered.com`, THEN continue as PC; Mobile, unknown, invalid, or + spoofed hosts return `needs_input`. +2. **Existing portal:** GIVEN the domain exists, WHEN onboarding starts, THEN + inspect and resume without duplicates; ambiguous matches require selection. +3. **Access:** GIVEN a mutation returns `401/403`, WHEN work is partial, THEN + preserve the ledger, return `needs_access`, reauthenticate, re-read, resume. +4. **Login:** GIVEN sign-in succeeds, WHEN binding fails, THEN Login and + onboarding remain incomplete until binding is verified. +5. **Publication:** GIVEN HTTP 200, WHEN an older version is visible, THEN + publication remains incomplete. +6. **Launcher:** GIVEN a Launcher exists, WHEN build, installer, or download + evidence is missing, THEN Launcher is incomplete and publish-anyway is + refused. +7. **Handoff:** GIVEN the run ends, WHEN reporting, THEN repeat merchant ID, + project ID, domain, Steam URL, and locale with evidence for every Completed + item. + +## Design + +### Flow + +```mermaid +flowchart LR + Intake --> Preflight + Preflight --> Discover + Discover --> Draft + Draft --> Verify + Verify --> HumanReview + HumanReview --> Publish + Publish --> LiveVerify + LiveVerify --> Handoff +``` + +### Status + +- `completed` — effect verified. +- `placeholder` — visible and temporary. +- `needs_input` — value or choice missing. +- `needs_access` — authorization invalid. +- `needs_human` — manual action required. +- `blocked_capability` — CLI cannot perform the action. +- `failed` — action failed or cannot be verified. + +### Evidence contract + +| State | Required evidence | Stop condition | +|---|---|---| +| Preflight | CLI context, exact Steam host, domain search | Missing/ambiguous input | +| Discover | Existing IDs, supported type and skills | Unsupported structure | +| Draft | Mutation response and read-back | Read-back mismatch | +| Verify | Structure, preview, readiness result | Pending/failed verification | +| HumanReview | Approval and disclosed gaps | Approval missing | +| Publish | Supported command or confirmed human action | Publish unconfirmed | +| LiveVerify | Public URL, expected version/routes, Login/commerce | Stale/incomplete state | +| Handoff | Full context, statuses, evidence, next actions | Completed lacks evidence | + +### Existing vs desired + +Before mutation: + +| Existing state | Desired state | Action | +|---|---|---| +| Verified entity ID and values | Requested change | Create, update, ask, or stop | + +Never recreate discovered existing entities. + +## Run checklist + +### 1. Context + +- Confirm merchant/project, domain, locale, and create/update policy. +- Validate exact Steam host. +- Resolve ambiguity and disclose unsupported/human gates. + +### 2. Existing state + +- List websites and read the target structure. +- Capture landing, page, and block IDs. +- Compare existing and desired state. +- Resume at the first incomplete item. + +### 3. One change group + +Portal structure — creation and landing type, pages, blocks, theme, assets, copy +and localization, domain, analytics, preview — runs against the Shop Builder API +in [portal-api.md](portal-api.md). Use related skills for everything else, +instead of repeating their command recipes: + +- `merchant-setup` — merchant/project/API key. +- `catalog-design` — catalog and pricing. +- `login-setup` — Login. +- `headless-checkout-integration` — checkout. + +Sections: Home, News, Rewards, Web Shop, Community, optional Launcher. +Placeholders require approval and visible labels. Launcher requires a real +Launcher, uploaded build, generated installer, and verified download. + +### 4. Verify + +- Read back changed entities. +- Refresh preview and confirm rendered output. +- Update the ledger. +- Keep unverified items out of **Completed**. + +### 5. Draft gate + +`draft_ready` requires correct ownership/domain/type/locale, no duplicate +routes, verified content, disclosed placeholders, explicit Login/commerce +status, and no readiness failure. + +### 6. Review and publish + +- Present Completed, placeholders, blockers, and failures. +- Require publication approval. +- Use a supported command or return `needs_human`. +- Resume after confirmed human publication. + +### 7. Live verification + +`published_verified` requires the expected public version/routes, working Login +and binding, and verified Web Shop/Launcher outcomes. HTTP 200 with stale +content is incomplete. + +### 8. Handoff + +```markdown +# Xsolla Game Web Portal onboarding report + +Overall status: + +## Confirmed context +- Merchant ID: +- Project ID: +- Domain: +- Steam URL: +- Primary locale: + +## Sections +| Section | Page ID | Route | Status | Evidence | +|---|---|---|---|---| + +## Completed +- Verified action + evidence + +## Placeholders +- Temporary content + label + +## Needs input / human action +- Action + owner + value + verification + +## Blocked capabilities +- Capability + impact + next step + +## Failed +- Action + error + recovery + +## Publication +- Status: +- Preview/public URL: +- Evidence: +``` + +Honest partial completion is correct. Simulated completion is failure. diff --git a/skills/game-web-portal/references/portal-api.md b/skills/game-web-portal/references/portal-api.md new file mode 100644 index 0000000..d03bbe6 --- /dev/null +++ b/skills/game-web-portal/references/portal-api.md @@ -0,0 +1,293 @@ +# Xsolla Game Web Portal — Shop Builder API reference + +The portal itself (sites, pages, blocks, theme, copy, domain, preview, publication) +is Shop Builder. This file is the API surface for Steps 3–8. Catalog, Login, and +checkout stay delegated — see `SKILL.md`. + +## Context + +- **Base URL:** `https://sitebuilder.xsolla.com/api` +- **Auth:** `Authorization` header carrying the Publisher Account admin token. + A missing, stale, or unauthorized token returns `401/403` → `needs_access`. + (The Xsolla CLI drives the same service with a publisher session cookie + `pa-v4-token` instead; either way, this is *not* `XSOLLA_PROJECT_API_KEY`.) +- **Path shorthand below:** `{M}` = `/merchant/{merchantId}/project/{projectId}` + +### Two different keys — the top cause of hard failures + +| Key | What it is | Used by | +|---|---|---| +| `domain` | the site's domain label, e.g. `voidwall` → `voidwall.xsolla.site` | `landing/{domain}/…`, localization, preview, publication, versions | +| `landingId` | the landing's Mongo `_id` (top-level `_id` in `structure`) | `ui/{landing}/…` (blocks, store, settings), `assets/{collectionId}/…` | + +Sending a domain into a `ui/*` path makes the backend parse it as an ObjectId and +return **500**. Resolve `landingId` once during Discover and reuse it. + +## Discover + +| Intent | Call | +|---|---| +| List sites in the project | `GET {M}/landings` | +| Read one site (incl. `_id` = `landingId`) | `GET {M}/landing/{domain}` | +| Read full structure — pages, blocks, IDs, ordering | `GET {M}/landing/{domain}/structure` | +| List pages | `GET {M}/landing/{domain}/pages` | +| Read one page | `GET {M}/landing/{domain}/pages/{pageId}` | +| Partner's projects | `GET /merchant/{merchantId}/projects/list` | +| Licensing agreements (publication gate) | `GET /merchant/merchants/{merchantId}/agreements` | + +Discover is mandatory before any mutation: it supplies `landingId`, page IDs, block +IDs, and current ordering, and it is how resume avoids building a duplicate portal. + +## Preflight — read the Steam title + +| Intent | Call | Body | +|---|---|---| +| Pull game info from the store URL | `GET {M}/landing/{domain}/parsing` | `{ "type": "steam" \| "gplay" \| "topup" \| "sellingpage", "target": "https://store.steampowered.com/app/…" }` | + +Use this to confirm the title before drafting; PC/Steam only per the skill's entry +conditions. Never substitute invented game metadata when parsing fails — return +`needs_input`. + +## Draft — bootstrap the portal + +Prefer the generated bootstrap over hand-assembling pages and blocks. + +| Step | Call | Body | +|---|---|---| +| Create the site | `POST {M}/landing/{domain}` | `{ "name": "", "type": "topup", "colorScheme"?, "theme"? }` | +| Generate structure from the store URL | `POST {M}/landing/{domain}/structure` | `{ "type": "steam", "target": "" }` | +| Or initialize a portal template | `POST {M}/landing/{domain}/portal` | single-page vs hub (multi-page) layout; theme derived from the game icon | +| Add a block-set template | `POST {M}/landing/{domain}/template` | `{ "type": "steam", "template": "home" \| "store" \| "news" }` | +| Finalize the landing type | `PUT {M}/landing/{domain}/admin/change-landing-type` | `{ "type": "topup" \| "store" \| "sellingpage" }` | + +Page templates available when adding a page (from the Publisher Account builder): +`Blank`, `Store`, `Rewards` (daily rewards and reward-system blocks), `News`, +`Loyalty shop`, `Promocodes`, `Single game` (accepts a Steam link and generates the +description, images, and styling from it), `Games catalog`, `Items store`. The +portal's Rewards and News sections map onto the templates of those names; there is +**no Community template** — that section needs a Blank page and explicit blocks, so +treat it as `needs_input` rather than guessing a layout. + +- `POST .../portal` only works on a landing with **no type assigned** — it returns + **409** once a portal structure exists. On resume, read the structure instead of + re-initializing. +- `structure` accepts `sellingpage`, `gplay`, `steam`, `store`, `topup`, `rfppage`, + `free2play`. Omit `target` for `sellingpage`. +- Without a finalized landing type the editor gates on a domain prompt and the + preview 404s. +- Other site-level calls: `POST {M}/landing/{domain}/duplicate`, + `PATCH {M}/landing/{domain}` (domain rename), `DELETE {M}/landing/{domain}` + (destructive — never without explicit approval), + `PUT {M}/landing/{domain}/admin/change-merchant` / `change-project`. + +## Draft — pages, navigation, features + +| Intent | Call | Body | +|---|---|---| +| Add page | `POST {M}/landing/{domain}/pages` | `{ "name": "<1–80 chars>", "path": "/main" }` | +| Update page | `PATCH {M}/landing/{domain}/pages/{pageId}` | page fields | +| Duplicate page | `POST {M}/landing/{domain}/pages/{pageId}` | — | +| Delete page | `DELETE {M}/landing/{domain}/pages/{pageId}` | — | +| Link a page under a parent (nav) | `POST {M}/landing/{domain}/linking` | `{ "parent": "", "path": "link-example" }` | +| Remove a link | `DELETE {M}/landing/{domain}/linking` | — | +| Toggle site features | `PATCH {M}/landing/{domain}/features` | feature list | +| Page settings | `PUT {M}/ui/{landing}/page/{pageId}/savepagesettings` | — | +| Site settings | `PUT {M}/ui/{landing}/savelandingsettings` | — | + +`path` accepts lowercase `a–z`, `0–9`, hyphen and slash only, max 80 chars. + +## Draft — blocks + +Keyed by `landingId`. + +| Intent | Call | Body | +|---|---|---| +| Add block | `POST {M}/ui/{landing}/page/{pageId}/block` | `{ "block": "", "index"?: <0-based> }` | +| Move block | `PUT {M}/ui/{landing}/page/{pageId}/block` | source/destination indices, 0-based | +| Delete block | `DELETE {M}/ui/{landing}/page/{pageId}/block` | block `_id` | +| Duplicate block | `POST {M}/ui/{landing}/page/{pageId}/block/duplicate` | `{ "blockId": "<_id>", "index"?: }` | +| Update a block | `PUT {M}/ui/{landing}/saveblock` | block payload | +| List available components | `GET {M}/ui/{landing}/components` | — | +| Batch patch blocks / pages / site | `PATCH {M}/ui/{landing}/batch` | see below | + +`block` is a **module template name**, not a block ID. Read what the project +actually offers from `GET {M}/ui/{landing}/components` or from `structure` before +adding — do not guess module names for News, Rewards, or Community. Known modules +include `lead` (hero), `newStore` (catalog grid), `federated`, `faq`, and the +default page scaffold (header, lead, description, packs, bento, gallery, +requirements, faq, footer). + +The batch endpoint is the call the editor itself makes (verified live; it is not in +the published catalog). Body is a map of `requestId → change`: + +```json +{"r1": {"type": "block", "id": "", + "patches": [{"op": "replace", "path": ["hidden"], "value": true}]}} +``` + +- `type` is `block` | `page` | `site`; `id` is the block `_id`, page `_id`, or the + `landingId` (site-level). +- `path` is an Immer segment array. `op` is `add` | `remove` | `replace`. +- Protected, un-patchable: `_id`, `module`, `blockVersion`. +- `POST {M}/ui/{landing}/page/{pageId}/block/changeVersion` is an internal UI + endpoint — do not call it. + +## Draft — Web Shop wiring + +| Intent | Call | +|---|---| +| Toggle a "Show in Store" component | `PUT {M}/ui/{landing}/toggleStoreComponent` — `{ "componentName": "subscriptions" }` | +| Virtual item groups | `GET {M}/ui/{landing}/store/virtualItems` | +| Goods in one group | `GET {M}/ui/{landing}/store/{groupId}` | +| Virtual currencies / packages | `GET {M}/ui/{landing}/store/virtual_currency`, `…/virtual_currency/package` | +| Game keys | `GET {M}/ui/{landing}/store/games` | +| Subscription plans | `GET {M}/ui/{landing}/subscriptionPlans` | +| Configured SKUs from PA | `GET {M}/ui/{landing}/sku` | +| Store API retry policy | `PUT {M}/landing/{domain}/store-api-retry` | + +Catalog contents themselves stay with `catalog-design`; these endpoints only bind an +existing catalog into the portal. + +## Draft — Launcher + +| Intent | Call | +|---|---| +| Launchers available to the project | `GET {M}/ui/{landing}/launcherList` → `[{ id, name }]` | +| Create a news item | `POST /launcher/{launcherId}/merchant/{merchantId}/landing/{landingId}/constructor/news` | +| Update / delete a news item | `PUT` / `DELETE …/constructor/news/{newsId}` | +| List news (constructor) | `GET /launcher/{launcherId}/constructor/news?offset=&limit=` | +| Read one news item | `GET /launcher/{launcherId}/constructor/news/{newsId}` | +| Public news feed | `GET /public/launcher/{launcherId}/project/{projectId}/news` | + +News articles are Launcher content, not page content: they live in Publisher Account +under **Distribution → Launcher → Content tiles** as content groups plus articles of +type `News`, each created in `Draft` and only visible once switched to `Publish`. +A launcher must exist before articles can be published — but it needs no games and +no Login configured for this purpose. A News section whose articles are still +`Draft` is `placeholder`, not `completed`. + +Launcher **builds, installers, and downloads are not in this API.** A Launcher is +only `completed` with a real Launcher on the project, an uploaded build, a generated +installer, and a verified installer download — evidence that must come from the +Launcher product itself. Missing it means `blocked_capability`, never `completed`. + +## Draft — theme and assets + +Theme is a `site` patch through the batch call: + +```json +{"t": {"type": "site", "id": "", + "patches": [{"op": "replace", + "path": ["theme", "mainColors", "accentColor"], + "value": "rgba(53,224,255,1)"}]}} +``` + +| Intent | Call | +|---|---| +| Theme as a CSS file | `GET {M}/landing/{domain}/theme` | +| List assets | `GET {M}/assets/{collectionId}/{collectionName}` | +| Upload asset (`multipart/form-data`, part `file`) | `POST {M}/assets/{collectionId}/{collectionName}` | +| Update / delete asset | `PATCH` / `DELETE {M}/assets/{collectionId}/{assetId}` | + +`collectionId` equals the `landingId`. Upload only partner-approved assets. + +## Draft — copy and localization + +**Block text does not live on the block.** Blocks reference an `L:` id and the text +lives in the localization store, so patching `["values","title"]` does nothing. + +| Intent | Call | Body | +|---|---|---| +| Read the whole store | `GET /localization/extract/{domain}` | — | +| Read one locale of one page | `GET /localization/{domain}/{locale}/{pageId}` | — | +| Set one string | `POST /localization/update/{domain}` | `{ "pageId", "id": "L:", "locale": "en-US", "value": "

" }` | +| Set many for one locale | `POST /localization/update-many/{domain}` | `{ "locale", "perScopeValues": { "": { "L:": { "translation": "

" } } } }` | +| Replace the whole store | `POST /localization/load/{domain}` | full common + pages | +| Add / remove a locale | `POST` / `DELETE {M}/landing/{domain}/language` | `{ "language": "en-US" }` | + +- Page strings live under `pages..texts."L:"`, shared strings under + `common."L:"` (pass `common` as the scope key). Keep the `L:` prefix. +- In `update-many` the per-id value **must** be `{ "translation": "" }`. Any + other shape returns 200 and writes an **empty** string for that locale — + destructive. Other locales on the same string are preserved. + +## Domain, analytics, access, Login + +| Intent | Call | Body | +|---|---|---| +| Attach / change / remove external domain | `POST` / `PATCH` / `DELETE {M}/landing/{domain}/domains` | `{ "domain": "shop.example.com" }` | +| Verify DNS | `GET {M}/landing/{domain}/domains/lookup` | — | +| Analytics connector | `PUT` / `DELETE {M}/landing/{domain}/applications` | `{ "type": "gtm" \| "ga", "value": "" }` | +| Access restrictions | `PATCH` / `DELETE {M}/landing/{domain}/restrictions` | restriction set | +| Create a Login project | `POST /login/projects?merchantId=` | — | +| Read Login config | `GET /login/configuration/{loginId}` | — | +| Login widget settings | `POST` / `GET` / `PUT /login/widget-customization/{loginId}` | — | +| Publish widget settings | `POST /login/widget-customization/{loginId}/publish` | — | + +Login *behaviour* — auth methods, JWT validation, account binding — stays with +`login-setup`. Sign-in succeeding is not binding succeeding; both must be verified. + +## Verify and preview + +| Intent | Call | +|---|---| +| Enable public preview (returns the token) | `GET /landing/{domain}/public-preview/enable-preview` | +| Get the public preview link | `GET /landing/{domain}/public-preview/public-preview-link` | +| Disable public preview | `GET /landing/{domain}/public-preview/disable-preview` | +| Render one page directly | `GET /preview/{domain}/{page}/{locale}` (optional `?version=`) | +| Readiness check before publish | `GET {M}/landing/{domain}/check` → `{ "checkSku": true }` | + +Re-read `structure` after every change group; a mutation response alone is not +evidence. `check` gates on required fields such as a non-empty SKU. + +## Publish and rollback + +| Intent | Call | +|---|---| +| Publish the site | `POST {M}/landing/{domain}/publication` | +| List archived versions | `GET {M}/landing/{domain}/versions` | +| Apply an archived version (rollback) | `PUT {M}/landing/{domain}/versions/{versionId}` | + +**Publication is per-page, not whole-site.** The builder publishes a *selection* of +pages — which is what the deployed `check` and `publication` calls mean by +`draftPagesIds`. Resolve the page IDs from `structure` and pass the ones being +published; publishing "the portal" without a selection is what produces a `400`. + +Preconditions, all checkable before the call: + +- No empty sections anywhere in the builder. +- The Xsolla licensing agreement is signed — `GET /merchant/merchants/{merchantId}/agreements`. +- The main page is already published, or included in this same selection. **Child + pages cannot be published before the main page**, so order the selection + accordingly or the call fails. + +Publication returns `domain`, `languages`, `last_published`, and `user_published`. +Publishing still requires explicit partner approval, and `published_verified` still +requires the public URL to serve the expected version and routes — a `200` from +`publication` is a receipt, not proof the live site is correct. + +## Known API issues + +Confirmed against a live portal, 2026-08-14: + +- The public preview URL can return **403** while the token response reports the + preview as enabled. The CLI reads `public-preview/public-preview-last-token`, + which is not part of the published API; use `public-preview-link` and fall back to + a structure read-back as Verify evidence. Report a persistent 403 as `failed` with + the response — never as `completed`. +- The readiness check returns **400** when `draftPagesIds` is missing. This is not a + capability gap: the parameter is the page selection described under Publish, and + the published contract simply omits it. Send the selected page IDs. The exact + field shape is **unverified** — it is in no spec available here, only in the + deployed endpoint and the builder UI — so confirm it against a live call before + relying on it, and treat a still-failing check as `failed`, not `completed`. + +## Failure → status mapping + +| Response | Status | Action | +|---|---|---| +| `401` / `403` | `needs_access` | preserve the ledger, reauthenticate, re-read state, resume | +| `404` on create | `needs_human` | Shop Builder is not enabled for the project; the partner enables it in Publisher Account | +| `409` from `POST .../portal` | — | the portal is already initialized: read the structure and resume instead of recreating | +| `500` from a `ui/*` path | — | wrong key: a domain was sent where `landingId` is required. Fix and retry; not a capability block | +| Launcher build / installer / download | `blocked_capability` | not exposed by this API | From 13e2d80fdb8452649f5659c8f8ec33b391758b7a Mon Sep 17 00:00:00 2001 From: Andrew Pyanzin Date: Tue, 18 Aug 2026 10:33:20 +0300 Subject: [PATCH 2/2] chore: sync .cursor/skills mirror for game-web-portal Generated from skills/game-web-portal/ with the documented sync steps in .github/workflows/sync-providers.yml. CLAUDE.md was already in sync with AGENTS.md, so that step is a no-op. --- .cursor/skills/game-web-portal/SKILL.md | 150 +++++++++ .../references/agentic-onboarding.md | 187 +++++++++++ .../game-web-portal/references/portal-api.md | 293 ++++++++++++++++++ 3 files changed, 630 insertions(+) create mode 100644 .cursor/skills/game-web-portal/SKILL.md create mode 100644 .cursor/skills/game-web-portal/references/agentic-onboarding.md create mode 100644 .cursor/skills/game-web-portal/references/portal-api.md diff --git a/.cursor/skills/game-web-portal/SKILL.md b/.cursor/skills/game-web-portal/SKILL.md new file mode 100644 index 0000000..12f0a56 --- /dev/null +++ b/.cursor/skills/game-web-portal/SKILL.md @@ -0,0 +1,150 @@ +--- +name: game-web-portal +description: >- + Set up, create, configure, or resume an Xsolla Game Web Portal for a PC or Steam + game. Use when onboarding a PC or Steam title, creating or resuming a Game Portal, + wiring a Web Shop, Launcher, or Login into a portal, generating a preview link, or + checking readiness and publication status. Covers the Home, News, Rewards, Web Shop, + Community, and optional Launcher sections plus catalog, theme, localization, preview, + and publication gates. Invoke for "set up my PC Game Portal", "resume my portal + without duplicating pages", "give me a verified preview", or "is my portal ready to + publish". PC and Steam only — App Store and Google Play onboarding is out of scope + and returns needs_input. +metadata: + owner: a.pyanzin + domain: orchestrator + status: draft +--- + +# Game Web Portal + +Create or resume a verified PC/Steam Game Web Portal and return an evidence-backed +partner handoff. Honest partial completion is correct; simulated completion is failure. + +## When to use + +Trigger keywords: PC Game Portal, Game Web Portal, Steam onboarding, portal resume, +Web Shop, Launcher, Login binding, preview link, readiness, publication. + +Entry conditions: + +- The title ships on PC and the store URL host is exactly `store.steampowered.com`. + Mobile, unknown, invalid, or spoofed hosts return `needs_input`. +- A publisher context is authenticated and a merchant/project pair is confirmed. +- The caller has decided whether an existing portal at the domain should be updated + or a new one created. + +Use `shop-setup` instead for a general zero-to-shop storefront; this skill is the +PC/Steam portal path and delegates catalog, Login, and checkout work to the skills +listed under Steps. + +## Prerequisites + +- Authenticated publisher context. +- Confirmed merchant ID and project ID — see `merchant-setup`. +- API key for catalog and payment operations where required. +- Shop Builder access for the target project. Portal operations authenticate with a + Publisher Account admin token, not the project API key — see + [references/portal-api.md](references/portal-api.md). Treat `401/403` as + `needs_access`, never as a capability block. +- Approved content and brand assets. Never invent or reuse partner identifiers, + credentials, content, prices, assets, or URLs. +- Required input, collected in a single question rather than one at a time: + +```yaml +merchant_id: +project_id: +domain: +game_name: +store_url: +primary_locale: +existing_portal_policy: update | create-new +``` + +Optional: approved logo, hero, screenshots, colors and fonts; an existing Store +catalog; additional locales; analytics IDs; an existing Launcher and build. + +## Steps + +The run is a state flow. Every mutation is preceded by an existing-vs-desired-state +check and followed by read-back and ledger update. + +```text +Intake → Preflight → Discover → Draft → Verify → Human review → +Publish → Live verification → Handoff +``` + +1. **Intake** — collect the required input above. Resolve every ambiguity by asking; + never choose an ambiguous match. +2. **Preflight** — validate the exact Steam host, read the title through + `GET .../landing/{domain}/parsing` with `type: steam`, confirm merchant/project, + domain, and locale, and disclose any unsupported or human-gated step up front. + Never substitute invented game metadata when parsing fails. +3. **Discover** — list existing portals and read the target structure + (`GET .../landings`, `GET .../landing/{domain}`, `GET .../landing/{domain}/structure`). + Capture the landing `_id`, page IDs, and block IDs before any mutation — block and + theme calls are keyed by landing `_id`, not the domain. Never recreate a discovered + existing entity; resume at the first incomplete item. +4. **Draft** — bootstrap first: generate the portal from the store URL + (`POST .../landing/{domain}/structure` with `type: steam`, or + `POST .../landing/{domain}/portal` on a landing with no type yet — it returns + `409` once initialized, which means resume, not recreate). Then apply one change + group at a time across Home, News, Rewards, Web Shop, Community, and optional + Launcher. Portal structure itself — pages, blocks, theme, assets, copy and + localization, domain, analytics, preview — runs against the Shop Builder API in + [references/portal-api.md](references/portal-api.md). Delegate the surrounding + products rather than duplicating their recipes: `merchant-setup` for + merchant/project/API key, `catalog-design` for catalog and pricing, `login-setup` + for Login, `headless-checkout-integration` for checkout. Placeholders require + approval and a visible label. +5. **Verify** — read back every changed entity (`structure`, page, and block reads), + refresh the preview, run the readiness check (`GET .../landing/{domain}/check`), + confirm rendered output, and update the ledger. A mutation response is not + evidence. Keep unverified items out of **Completed**. +6. **Human review** — present completed items, placeholders, blockers, and failures. + `draft_ready` requires correct ownership, domain, type, and locale, no duplicate + routes, verified content, disclosed placeholders, explicit Login and commerce + status, and no readiness failure. +7. **Publish** — require explicit approval, then `POST .../landing/{domain}/publication` + with the selected page IDs: publication is per-page, the main page must be live or + in the same selection, no section may be empty, and the licensing agreement must be + signed. Never publish automatically, and never treat the `200` as proof the live + site is correct — it is a receipt, and `last_published` still has to be confirmed + against the public URL in Step 8. Rollback is `GET .../landing/{domain}/versions` + plus `PUT .../landing/{domain}/versions/{versionId}`. +8. **Live verification** — `published_verified` requires the expected public version + and routes, working Login and account binding, and verified Web Shop and Launcher + outcomes. +9. **Handoff** — repeat merchant ID, project ID, domain, Steam URL, and locale, with + evidence for every completed item. + +Status values: `completed`, `placeholder`, `needs_input`, `needs_access`, +`needs_human`, `blocked_capability`, `failed`. + +Two references, both loaded before issuing changes: + +- [references/agentic-onboarding.md](references/agentic-onboarding.md) — the full + specification: `GIVEN / WHEN / THEN` acceptance scenarios, the per-state evidence + contract, and the handoff report template. +- [references/portal-api.md](references/portal-api.md) — the Shop Builder API for + Steps 2–8: endpoints per stage, the domain vs landing `_id` split, the localization + payload shape, known API issues, and the response → status mapping. + +## Common pitfalls + +1. **Duplicate pages and blocks on resume.** Onboarding an existing domain without + reading the current structure first creates a second Home or Web Shop. Always + discover and compare existing versus desired state before any mutation. +2. **HTTP 200 read as published.** A live URL can serve an older version. Publication + stays incomplete until the expected version and routes are confirmed live. +3. **Login sign-in mistaken for Login done.** Sign-in can succeed while account + binding fails. Both Login and onboarding remain incomplete until binding is + verified. +4. **Launcher marked complete without a verified download.** A Launcher needs a real + Launcher on the project, an uploaded build, a generated installer, and a verified + installer download. Refuse publish-anyway when that evidence is missing. +5. **Access expiry losing progress.** On `401/403` mid-run, preserve the ledger and + return `needs_access`, then reauthenticate, re-read state, and resume — do not + restart the portal. +6. **Reporting completion without read-back.** Nothing enters **Completed** while + read-back, rendered output, or end-to-end verification is pending. diff --git a/.cursor/skills/game-web-portal/references/agentic-onboarding.md b/.cursor/skills/game-web-portal/references/agentic-onboarding.md new file mode 100644 index 0000000..218d1fe --- /dev/null +++ b/.cursor/skills/game-web-portal/references/agentic-onboarding.md @@ -0,0 +1,187 @@ +# Xsolla Game Web Portal — Agentic Onboarding Specification + +## Requirements + +### Input + +Collect missing values in one question: + +```yaml +merchant_id: +project_id: +domain: +game_name: +store_url: +primary_locale: +existing_portal_policy: update | create-new +``` + +Optional: approved brand assets, analytics IDs, and locales. Never invent or +reuse partner identifiers, credentials, content, prices, assets, or URLs. + +### Acceptance scenarios + +1. **Platform:** GIVEN a store URL, WHEN the exact host is + `store.steampowered.com`, THEN continue as PC; Mobile, unknown, invalid, or + spoofed hosts return `needs_input`. +2. **Existing portal:** GIVEN the domain exists, WHEN onboarding starts, THEN + inspect and resume without duplicates; ambiguous matches require selection. +3. **Access:** GIVEN a mutation returns `401/403`, WHEN work is partial, THEN + preserve the ledger, return `needs_access`, reauthenticate, re-read, resume. +4. **Login:** GIVEN sign-in succeeds, WHEN binding fails, THEN Login and + onboarding remain incomplete until binding is verified. +5. **Publication:** GIVEN HTTP 200, WHEN an older version is visible, THEN + publication remains incomplete. +6. **Launcher:** GIVEN a Launcher exists, WHEN build, installer, or download + evidence is missing, THEN Launcher is incomplete and publish-anyway is + refused. +7. **Handoff:** GIVEN the run ends, WHEN reporting, THEN repeat merchant ID, + project ID, domain, Steam URL, and locale with evidence for every Completed + item. + +## Design + +### Flow + +```mermaid +flowchart LR + Intake --> Preflight + Preflight --> Discover + Discover --> Draft + Draft --> Verify + Verify --> HumanReview + HumanReview --> Publish + Publish --> LiveVerify + LiveVerify --> Handoff +``` + +### Status + +- `completed` — effect verified. +- `placeholder` — visible and temporary. +- `needs_input` — value or choice missing. +- `needs_access` — authorization invalid. +- `needs_human` — manual action required. +- `blocked_capability` — CLI cannot perform the action. +- `failed` — action failed or cannot be verified. + +### Evidence contract + +| State | Required evidence | Stop condition | +|---|---|---| +| Preflight | CLI context, exact Steam host, domain search | Missing/ambiguous input | +| Discover | Existing IDs, supported type and skills | Unsupported structure | +| Draft | Mutation response and read-back | Read-back mismatch | +| Verify | Structure, preview, readiness result | Pending/failed verification | +| HumanReview | Approval and disclosed gaps | Approval missing | +| Publish | Supported command or confirmed human action | Publish unconfirmed | +| LiveVerify | Public URL, expected version/routes, Login/commerce | Stale/incomplete state | +| Handoff | Full context, statuses, evidence, next actions | Completed lacks evidence | + +### Existing vs desired + +Before mutation: + +| Existing state | Desired state | Action | +|---|---|---| +| Verified entity ID and values | Requested change | Create, update, ask, or stop | + +Never recreate discovered existing entities. + +## Run checklist + +### 1. Context + +- Confirm merchant/project, domain, locale, and create/update policy. +- Validate exact Steam host. +- Resolve ambiguity and disclose unsupported/human gates. + +### 2. Existing state + +- List websites and read the target structure. +- Capture landing, page, and block IDs. +- Compare existing and desired state. +- Resume at the first incomplete item. + +### 3. One change group + +Portal structure — creation and landing type, pages, blocks, theme, assets, copy +and localization, domain, analytics, preview — runs against the Shop Builder API +in [portal-api.md](portal-api.md). Use related skills for everything else, +instead of repeating their command recipes: + +- `merchant-setup` — merchant/project/API key. +- `catalog-design` — catalog and pricing. +- `login-setup` — Login. +- `headless-checkout-integration` — checkout. + +Sections: Home, News, Rewards, Web Shop, Community, optional Launcher. +Placeholders require approval and visible labels. Launcher requires a real +Launcher, uploaded build, generated installer, and verified download. + +### 4. Verify + +- Read back changed entities. +- Refresh preview and confirm rendered output. +- Update the ledger. +- Keep unverified items out of **Completed**. + +### 5. Draft gate + +`draft_ready` requires correct ownership/domain/type/locale, no duplicate +routes, verified content, disclosed placeholders, explicit Login/commerce +status, and no readiness failure. + +### 6. Review and publish + +- Present Completed, placeholders, blockers, and failures. +- Require publication approval. +- Use a supported command or return `needs_human`. +- Resume after confirmed human publication. + +### 7. Live verification + +`published_verified` requires the expected public version/routes, working Login +and binding, and verified Web Shop/Launcher outcomes. HTTP 200 with stale +content is incomplete. + +### 8. Handoff + +```markdown +# Xsolla Game Web Portal onboarding report + +Overall status: + +## Confirmed context +- Merchant ID: +- Project ID: +- Domain: +- Steam URL: +- Primary locale: + +## Sections +| Section | Page ID | Route | Status | Evidence | +|---|---|---|---|---| + +## Completed +- Verified action + evidence + +## Placeholders +- Temporary content + label + +## Needs input / human action +- Action + owner + value + verification + +## Blocked capabilities +- Capability + impact + next step + +## Failed +- Action + error + recovery + +## Publication +- Status: +- Preview/public URL: +- Evidence: +``` + +Honest partial completion is correct. Simulated completion is failure. diff --git a/.cursor/skills/game-web-portal/references/portal-api.md b/.cursor/skills/game-web-portal/references/portal-api.md new file mode 100644 index 0000000..d03bbe6 --- /dev/null +++ b/.cursor/skills/game-web-portal/references/portal-api.md @@ -0,0 +1,293 @@ +# Xsolla Game Web Portal — Shop Builder API reference + +The portal itself (sites, pages, blocks, theme, copy, domain, preview, publication) +is Shop Builder. This file is the API surface for Steps 3–8. Catalog, Login, and +checkout stay delegated — see `SKILL.md`. + +## Context + +- **Base URL:** `https://sitebuilder.xsolla.com/api` +- **Auth:** `Authorization` header carrying the Publisher Account admin token. + A missing, stale, or unauthorized token returns `401/403` → `needs_access`. + (The Xsolla CLI drives the same service with a publisher session cookie + `pa-v4-token` instead; either way, this is *not* `XSOLLA_PROJECT_API_KEY`.) +- **Path shorthand below:** `{M}` = `/merchant/{merchantId}/project/{projectId}` + +### Two different keys — the top cause of hard failures + +| Key | What it is | Used by | +|---|---|---| +| `domain` | the site's domain label, e.g. `voidwall` → `voidwall.xsolla.site` | `landing/{domain}/…`, localization, preview, publication, versions | +| `landingId` | the landing's Mongo `_id` (top-level `_id` in `structure`) | `ui/{landing}/…` (blocks, store, settings), `assets/{collectionId}/…` | + +Sending a domain into a `ui/*` path makes the backend parse it as an ObjectId and +return **500**. Resolve `landingId` once during Discover and reuse it. + +## Discover + +| Intent | Call | +|---|---| +| List sites in the project | `GET {M}/landings` | +| Read one site (incl. `_id` = `landingId`) | `GET {M}/landing/{domain}` | +| Read full structure — pages, blocks, IDs, ordering | `GET {M}/landing/{domain}/structure` | +| List pages | `GET {M}/landing/{domain}/pages` | +| Read one page | `GET {M}/landing/{domain}/pages/{pageId}` | +| Partner's projects | `GET /merchant/{merchantId}/projects/list` | +| Licensing agreements (publication gate) | `GET /merchant/merchants/{merchantId}/agreements` | + +Discover is mandatory before any mutation: it supplies `landingId`, page IDs, block +IDs, and current ordering, and it is how resume avoids building a duplicate portal. + +## Preflight — read the Steam title + +| Intent | Call | Body | +|---|---|---| +| Pull game info from the store URL | `GET {M}/landing/{domain}/parsing` | `{ "type": "steam" \| "gplay" \| "topup" \| "sellingpage", "target": "https://store.steampowered.com/app/…" }` | + +Use this to confirm the title before drafting; PC/Steam only per the skill's entry +conditions. Never substitute invented game metadata when parsing fails — return +`needs_input`. + +## Draft — bootstrap the portal + +Prefer the generated bootstrap over hand-assembling pages and blocks. + +| Step | Call | Body | +|---|---|---| +| Create the site | `POST {M}/landing/{domain}` | `{ "name": "", "type": "topup", "colorScheme"?, "theme"? }` | +| Generate structure from the store URL | `POST {M}/landing/{domain}/structure` | `{ "type": "steam", "target": "" }` | +| Or initialize a portal template | `POST {M}/landing/{domain}/portal` | single-page vs hub (multi-page) layout; theme derived from the game icon | +| Add a block-set template | `POST {M}/landing/{domain}/template` | `{ "type": "steam", "template": "home" \| "store" \| "news" }` | +| Finalize the landing type | `PUT {M}/landing/{domain}/admin/change-landing-type` | `{ "type": "topup" \| "store" \| "sellingpage" }` | + +Page templates available when adding a page (from the Publisher Account builder): +`Blank`, `Store`, `Rewards` (daily rewards and reward-system blocks), `News`, +`Loyalty shop`, `Promocodes`, `Single game` (accepts a Steam link and generates the +description, images, and styling from it), `Games catalog`, `Items store`. The +portal's Rewards and News sections map onto the templates of those names; there is +**no Community template** — that section needs a Blank page and explicit blocks, so +treat it as `needs_input` rather than guessing a layout. + +- `POST .../portal` only works on a landing with **no type assigned** — it returns + **409** once a portal structure exists. On resume, read the structure instead of + re-initializing. +- `structure` accepts `sellingpage`, `gplay`, `steam`, `store`, `topup`, `rfppage`, + `free2play`. Omit `target` for `sellingpage`. +- Without a finalized landing type the editor gates on a domain prompt and the + preview 404s. +- Other site-level calls: `POST {M}/landing/{domain}/duplicate`, + `PATCH {M}/landing/{domain}` (domain rename), `DELETE {M}/landing/{domain}` + (destructive — never without explicit approval), + `PUT {M}/landing/{domain}/admin/change-merchant` / `change-project`. + +## Draft — pages, navigation, features + +| Intent | Call | Body | +|---|---|---| +| Add page | `POST {M}/landing/{domain}/pages` | `{ "name": "<1–80 chars>", "path": "/main" }` | +| Update page | `PATCH {M}/landing/{domain}/pages/{pageId}` | page fields | +| Duplicate page | `POST {M}/landing/{domain}/pages/{pageId}` | — | +| Delete page | `DELETE {M}/landing/{domain}/pages/{pageId}` | — | +| Link a page under a parent (nav) | `POST {M}/landing/{domain}/linking` | `{ "parent": "", "path": "link-example" }` | +| Remove a link | `DELETE {M}/landing/{domain}/linking` | — | +| Toggle site features | `PATCH {M}/landing/{domain}/features` | feature list | +| Page settings | `PUT {M}/ui/{landing}/page/{pageId}/savepagesettings` | — | +| Site settings | `PUT {M}/ui/{landing}/savelandingsettings` | — | + +`path` accepts lowercase `a–z`, `0–9`, hyphen and slash only, max 80 chars. + +## Draft — blocks + +Keyed by `landingId`. + +| Intent | Call | Body | +|---|---|---| +| Add block | `POST {M}/ui/{landing}/page/{pageId}/block` | `{ "block": "", "index"?: <0-based> }` | +| Move block | `PUT {M}/ui/{landing}/page/{pageId}/block` | source/destination indices, 0-based | +| Delete block | `DELETE {M}/ui/{landing}/page/{pageId}/block` | block `_id` | +| Duplicate block | `POST {M}/ui/{landing}/page/{pageId}/block/duplicate` | `{ "blockId": "<_id>", "index"?: }` | +| Update a block | `PUT {M}/ui/{landing}/saveblock` | block payload | +| List available components | `GET {M}/ui/{landing}/components` | — | +| Batch patch blocks / pages / site | `PATCH {M}/ui/{landing}/batch` | see below | + +`block` is a **module template name**, not a block ID. Read what the project +actually offers from `GET {M}/ui/{landing}/components` or from `structure` before +adding — do not guess module names for News, Rewards, or Community. Known modules +include `lead` (hero), `newStore` (catalog grid), `federated`, `faq`, and the +default page scaffold (header, lead, description, packs, bento, gallery, +requirements, faq, footer). + +The batch endpoint is the call the editor itself makes (verified live; it is not in +the published catalog). Body is a map of `requestId → change`: + +```json +{"r1": {"type": "block", "id": "", + "patches": [{"op": "replace", "path": ["hidden"], "value": true}]}} +``` + +- `type` is `block` | `page` | `site`; `id` is the block `_id`, page `_id`, or the + `landingId` (site-level). +- `path` is an Immer segment array. `op` is `add` | `remove` | `replace`. +- Protected, un-patchable: `_id`, `module`, `blockVersion`. +- `POST {M}/ui/{landing}/page/{pageId}/block/changeVersion` is an internal UI + endpoint — do not call it. + +## Draft — Web Shop wiring + +| Intent | Call | +|---|---| +| Toggle a "Show in Store" component | `PUT {M}/ui/{landing}/toggleStoreComponent` — `{ "componentName": "subscriptions" }` | +| Virtual item groups | `GET {M}/ui/{landing}/store/virtualItems` | +| Goods in one group | `GET {M}/ui/{landing}/store/{groupId}` | +| Virtual currencies / packages | `GET {M}/ui/{landing}/store/virtual_currency`, `…/virtual_currency/package` | +| Game keys | `GET {M}/ui/{landing}/store/games` | +| Subscription plans | `GET {M}/ui/{landing}/subscriptionPlans` | +| Configured SKUs from PA | `GET {M}/ui/{landing}/sku` | +| Store API retry policy | `PUT {M}/landing/{domain}/store-api-retry` | + +Catalog contents themselves stay with `catalog-design`; these endpoints only bind an +existing catalog into the portal. + +## Draft — Launcher + +| Intent | Call | +|---|---| +| Launchers available to the project | `GET {M}/ui/{landing}/launcherList` → `[{ id, name }]` | +| Create a news item | `POST /launcher/{launcherId}/merchant/{merchantId}/landing/{landingId}/constructor/news` | +| Update / delete a news item | `PUT` / `DELETE …/constructor/news/{newsId}` | +| List news (constructor) | `GET /launcher/{launcherId}/constructor/news?offset=&limit=` | +| Read one news item | `GET /launcher/{launcherId}/constructor/news/{newsId}` | +| Public news feed | `GET /public/launcher/{launcherId}/project/{projectId}/news` | + +News articles are Launcher content, not page content: they live in Publisher Account +under **Distribution → Launcher → Content tiles** as content groups plus articles of +type `News`, each created in `Draft` and only visible once switched to `Publish`. +A launcher must exist before articles can be published — but it needs no games and +no Login configured for this purpose. A News section whose articles are still +`Draft` is `placeholder`, not `completed`. + +Launcher **builds, installers, and downloads are not in this API.** A Launcher is +only `completed` with a real Launcher on the project, an uploaded build, a generated +installer, and a verified installer download — evidence that must come from the +Launcher product itself. Missing it means `blocked_capability`, never `completed`. + +## Draft — theme and assets + +Theme is a `site` patch through the batch call: + +```json +{"t": {"type": "site", "id": "", + "patches": [{"op": "replace", + "path": ["theme", "mainColors", "accentColor"], + "value": "rgba(53,224,255,1)"}]}} +``` + +| Intent | Call | +|---|---| +| Theme as a CSS file | `GET {M}/landing/{domain}/theme` | +| List assets | `GET {M}/assets/{collectionId}/{collectionName}` | +| Upload asset (`multipart/form-data`, part `file`) | `POST {M}/assets/{collectionId}/{collectionName}` | +| Update / delete asset | `PATCH` / `DELETE {M}/assets/{collectionId}/{assetId}` | + +`collectionId` equals the `landingId`. Upload only partner-approved assets. + +## Draft — copy and localization + +**Block text does not live on the block.** Blocks reference an `L:` id and the text +lives in the localization store, so patching `["values","title"]` does nothing. + +| Intent | Call | Body | +|---|---|---| +| Read the whole store | `GET /localization/extract/{domain}` | — | +| Read one locale of one page | `GET /localization/{domain}/{locale}/{pageId}` | — | +| Set one string | `POST /localization/update/{domain}` | `{ "pageId", "id": "L:", "locale": "en-US", "value": "

" }` | +| Set many for one locale | `POST /localization/update-many/{domain}` | `{ "locale", "perScopeValues": { "": { "L:": { "translation": "

" } } } }` | +| Replace the whole store | `POST /localization/load/{domain}` | full common + pages | +| Add / remove a locale | `POST` / `DELETE {M}/landing/{domain}/language` | `{ "language": "en-US" }` | + +- Page strings live under `pages..texts."L:"`, shared strings under + `common."L:"` (pass `common` as the scope key). Keep the `L:` prefix. +- In `update-many` the per-id value **must** be `{ "translation": "" }`. Any + other shape returns 200 and writes an **empty** string for that locale — + destructive. Other locales on the same string are preserved. + +## Domain, analytics, access, Login + +| Intent | Call | Body | +|---|---|---| +| Attach / change / remove external domain | `POST` / `PATCH` / `DELETE {M}/landing/{domain}/domains` | `{ "domain": "shop.example.com" }` | +| Verify DNS | `GET {M}/landing/{domain}/domains/lookup` | — | +| Analytics connector | `PUT` / `DELETE {M}/landing/{domain}/applications` | `{ "type": "gtm" \| "ga", "value": "" }` | +| Access restrictions | `PATCH` / `DELETE {M}/landing/{domain}/restrictions` | restriction set | +| Create a Login project | `POST /login/projects?merchantId=` | — | +| Read Login config | `GET /login/configuration/{loginId}` | — | +| Login widget settings | `POST` / `GET` / `PUT /login/widget-customization/{loginId}` | — | +| Publish widget settings | `POST /login/widget-customization/{loginId}/publish` | — | + +Login *behaviour* — auth methods, JWT validation, account binding — stays with +`login-setup`. Sign-in succeeding is not binding succeeding; both must be verified. + +## Verify and preview + +| Intent | Call | +|---|---| +| Enable public preview (returns the token) | `GET /landing/{domain}/public-preview/enable-preview` | +| Get the public preview link | `GET /landing/{domain}/public-preview/public-preview-link` | +| Disable public preview | `GET /landing/{domain}/public-preview/disable-preview` | +| Render one page directly | `GET /preview/{domain}/{page}/{locale}` (optional `?version=`) | +| Readiness check before publish | `GET {M}/landing/{domain}/check` → `{ "checkSku": true }` | + +Re-read `structure` after every change group; a mutation response alone is not +evidence. `check` gates on required fields such as a non-empty SKU. + +## Publish and rollback + +| Intent | Call | +|---|---| +| Publish the site | `POST {M}/landing/{domain}/publication` | +| List archived versions | `GET {M}/landing/{domain}/versions` | +| Apply an archived version (rollback) | `PUT {M}/landing/{domain}/versions/{versionId}` | + +**Publication is per-page, not whole-site.** The builder publishes a *selection* of +pages — which is what the deployed `check` and `publication` calls mean by +`draftPagesIds`. Resolve the page IDs from `structure` and pass the ones being +published; publishing "the portal" without a selection is what produces a `400`. + +Preconditions, all checkable before the call: + +- No empty sections anywhere in the builder. +- The Xsolla licensing agreement is signed — `GET /merchant/merchants/{merchantId}/agreements`. +- The main page is already published, or included in this same selection. **Child + pages cannot be published before the main page**, so order the selection + accordingly or the call fails. + +Publication returns `domain`, `languages`, `last_published`, and `user_published`. +Publishing still requires explicit partner approval, and `published_verified` still +requires the public URL to serve the expected version and routes — a `200` from +`publication` is a receipt, not proof the live site is correct. + +## Known API issues + +Confirmed against a live portal, 2026-08-14: + +- The public preview URL can return **403** while the token response reports the + preview as enabled. The CLI reads `public-preview/public-preview-last-token`, + which is not part of the published API; use `public-preview-link` and fall back to + a structure read-back as Verify evidence. Report a persistent 403 as `failed` with + the response — never as `completed`. +- The readiness check returns **400** when `draftPagesIds` is missing. This is not a + capability gap: the parameter is the page selection described under Publish, and + the published contract simply omits it. Send the selected page IDs. The exact + field shape is **unverified** — it is in no spec available here, only in the + deployed endpoint and the builder UI — so confirm it against a live call before + relying on it, and treat a still-failing check as `failed`, not `completed`. + +## Failure → status mapping + +| Response | Status | Action | +|---|---|---| +| `401` / `403` | `needs_access` | preserve the ledger, reauthenticate, re-read state, resume | +| `404` on create | `needs_human` | Shop Builder is not enabled for the project; the partner enables it in Publisher Account | +| `409` from `POST .../portal` | — | the portal is already initialized: read the structure and resume instead of recreating | +| `500` from a `ui/*` path | — | wrong key: a domain was sent where `landingId` is required. Fix and retry; not a capability block | +| Launcher build / installer / download | `blocked_capability` | not exposed by this API |