The first ads slice: a voluntary rewarded video credits chips. VK Mini App ads
(VKWebAppShowNativeAds) expose only a client-side watch result — no
server-to-server verify — so the credit is client-attested, guarded by a server
daily + hourly cap (config reward_daily_cap / reward_hourly_cap, default 50 / 10).
The caps are both anti-abuse (bounding a forger who skips the ad and calls the
endpoint directly) and an economic conversion lever (limiting free chips so a
player who wants more buys). D29 amended to VK's reality.
Backend: CreditReward (VK-only, order-less, idempotent on a client nonce, floored
by the caps; payout from config rewarded_payout_chips, default 0 = off) + the
wallet.reward edge op returning the updated wallet (reward_chips gates the
"watch for chips" CTA). Additive migration (two config columns).
Client: the ads-network abstraction (lib/ads.ts, VK impl) + the VK bridge
(vkRewardedReady / vkShowRewarded) + the Wallet CTA + i18n. A contour test stub
(VITE_ADS_STUB -> a toast instead of a real ad; prod always real) and a temporary
diagnostic that logs the raw VK data, so we confirm on the contour exactly what
VK returns (harden to signature-verify if it carries one).
Tests: backend integration (credit, nonce idempotency, hourly cap, disabled,
non-VK refusal) + codec unit (reward wire). Docs: PAYMENTS(+ru) §10, D29 amend,
PLAN E6. Bundle: shared budget 30->31 (reward i18n strings).
Tapping one pack's Buy dimmed every buy button at once (the shared busy
flag drove disabled + the :disabled opacity on all of them), so it looked
like both packs reacted to one tap. Track the in-flight product id
(busyId): the concurrency guard stays global, but the pressed/dimmed look
is now scoped to the tapped button. Payments were never affected.
Accept real money via Telegram Stars (XTR) — the third intake rail
alongside Robokassa (direct) and VK Votes.
Only the bot reaches Telegram, so the rail funnels through the reverse
mTLS bot-link:
- the gateway mints the invoice on a CreateInvoice command (the bot
calls createInvoiceLink, XTR; the link goes to WebApp.openInvoice);
- the bot gates each pre_checkout_query via a ValidatePreCheckout unary
(the order must exist, be still creditable and not already paid — the
reusable-invoice double-pay guard; the decline reason is localised to
the order account's language);
- a completed successful_payment is queued in a durable pure-Go SQLite
outbox and forwarded via a ForwardPayment unary, credited once
(idempotent on telegram_payment_charge_id, honours an expired order),
re-driven on restart and every 30s.
The rail is wired by TELEGRAM_STARS_OUTBOX_DIR (default /data) but stays
inert until a chip pack carries an XTR price, so seeding a Stars price in
the admin is the go-live.
Tests: backend integration (order->forward->credit once, duplicate,
pre_checkout gate) + bot outbox unit (idempotent, restart re-drive) +
executor createInvoice. Docs: PAYMENTS(+ru) §9, ARCHITECTURE, the
platform/telegram README, PLAN.
Replace the hardcoded erudit-game.ru host with location.origin, so the iOS
store note's "other version" link points at this deployment's own site root
(prod or the contour) without a hardcoded host or build-arg wiring.
The VK-iOS detection used platformSubtype(), which is server-derived for VK and
so never reported iOS on the client — the pack CTA stayed active and a tap hit a
403 with a generic error. Read the device family from the signed vk_platform
launch param (normalizeSubtype(vkPlatform())) instead. Under the store header on
VK-iOS, add a note that purchases are prohibited by Apple policy, linking to the
web version (opened through VK's external-link route).
Money purchases are not permitted in the VK iOS app (Apple ToS), and the backend
already refuses them (the CreateOrder VK-iOS freeze). Show the pack "Buy" muted
there and explain on tap ("purchases are not available on this platform"),
instead of letting the tap hit a 403 and a generic error toast. The storefront
still lists the packs.
Wire the VK Mini Apps ("голоса") rail end to end, reusing the intake engine. The
wallet.order endpoint branches by rail: a VK context opens a pending order
(provider vk) and returns its id, which the client passes to VKWebAppShowOrderBox.
VK's two-phase payment callback is verified at the gateway with the app protected
key (GATEWAY_VK_APP_SECRET) and proxied to a backend intake handler: get_item
returns the ordered pack's title and vote price; a chargeable order_status_change
credits the vk segment exactly once (the same Fund, idempotent on VK's own order
id) and records a succeeded event, so the dispatcher push refreshes the wallet.
Integration test for the VK order->credit path.
Deliver payment_events to connected clients as an in-app wallet-refresh push: a
background dispatcher drains undispatched events and publishes a KindNotification
"payment" signal, marking each delivered; the client bumps a wallet-refresh
counter the open Wallet screen watches, re-fetching in place. A return-focus
refetch is the fallback. The Robokassa Success/Fail return now serves a
self-closing page (the payment opens in a separate window) so the customer drops
back into the live app instead of a cold start. Integration test for the event
drain/mark queue.
Replace the disabled "Soon" pack action with a real purchase: the Wallet opens a
money order (wallet.order) and sends the player to the provider's hosted-payment
page (window.open via openExternalUrl); the chips are credited later by the
verified server callback. Add a public-offer link under the packs (paying
accepts the offer). Codec order round-trip unit test + a mock-e2e purchase test;
the Google Play stub and the chip-spend paths are unchanged.
Add the "Кошелёк" section to the settings hub: context-visible chip
balances, active benefits (no-ads term/forever, hints) and a storefront of
chip-priced values and money-priced chip packs. Guests have no wallet; the
Google Play build hides the money purchases behind a RuStore stub; a web
purchase that would draw VK/Telegram chips warns first.
Add the catalog read path the storefront needs — a context-projected
GET /api/v1/user/wallet/catalog (payments service + store, gateway op
wallet.catalog, FBS Catalog/CatalogProduct/CatalogAtom, client decode) —
plus the client leg for the existing wallet.get/buy ops. Value spends reuse
the existing spend path; the chip-pack purchase (money order flow) arrives
with payment intake, so its action is a disabled placeholder for now.
Covered by Go unit (catalog projection) + integration (/wallet/catalog over
Postgres), vitest (formatting, spendable selection, web-spend warning, GP
flag, codec + gateway encode round-trips) and Playwright mock e2e (render,
guest-hidden, GP stub, warning) on Chromium + WebKit.