cheapo — CLI in Go (Cobra) to fetch and observe current free game offers. Later planned: exposing the tools as an MCP server (see README.md, section "MCP server").
free-games-claimer — https://github.com/DoSpamu/free-games-claimer (fork of https://github.com/vogler/free-games-claimer). How it gets the free-games info:
run.js, Discord notify,
logger) on the upstream Docker image. The actual logic lives upstream in
vogler/free-games-claimer (epic-games.js, prime-gaming.js, gog.js,
steam-games.js, aliexpress.js).a:has(span:text-is("Free Now")), follows the links and claims them in the
browser (login per cookie session, 2FA via OTP key, captcha handling).epic-games.js, ~line 151): the page is built
from the JSON of the public backend:
https://store-site-backend-static.ak.epicgames.com/freeGamesPromotions
(filter data.Catalog.searchStore.elements on active promotions.promotionalOffers
with 100 % discount; slug preferably from catalogNs.mappings[0].pageSlug, since
urlSlug is sometimes wrong).free-games-notifier — https://github.com/jaixmario/free-games-notifier
(Python, GitHub-Actions cron every 6 h; notifications via e-mail, Telegram,
Discord, WhatsApp; state in free.json/free-steam.json).
How it gets the info:
epic.py): same public endpoint
store-site-backend-static.ak.epicgames.com/freeGamesPromotions?locale=en-US
(HTTP GET, no login) — confirms our Go approach. Filter:
discountSetting.discountPercentage == 0; distinguishes current
(promotionalOffers) and upcoming (upcomingPromotionalOffers — preview
window for future free games). Change detection via signature
(CURRENT:t1,t2|UPCOMING:...) + detail comparison per (title, link) keys;
only notify on new offers, removals are silently written to state.steam.py), two sources:
https://store.steampowered.com/search/?maxprice=free&specials=1
(BeautifulSoup; .search_result_row, price "Free"/100 %).https://store.steampowered.com/api/featuredcategories/ — items with
"Free Weekend"/"play for free" in name/body; title fallback via
store.steampowered.com/api/appdetails?appids=<id>.go version).gofmt -w . (mandatory before every commit; CI checks gofmt -l . = empty).gopls (install: go install golang.org/x/tools/gopls@latest).
Use in editors as LSP; additionally go vet ./... before every commit.bash
go build ./... && go vet ./... && go test ./...
gofmt -l . # must be empty
## Conventions
- Module path: gogs.netdome.biz/paul/cheapo.
- CLI framework: Cobra (github.com/spf13/cobra), commands in cmd/cheapo/main.go.
- Internal code under internal/ (epic: store client, watch: polling/eventing).
- Provider pattern: every store lives in internal/<store>/ and implements
game.Provider (Name() + GetFreeGames(ctx) ([]game.Game, error)); the
shared type is game.Game (internal/game). New stores (e.g. GOG)
follow this pattern and are registered in providers.New() (internal/providers)
— that is the only place CLI and MCP server share.
- MCP server: internal/mcpserver wraps only the tool list_offers
(equivalent to cheapo list, mode via the mode argument) over
github.com/modelcontextprotocol/go-sdk. No store logic there;
responses are structured (structuredContent) and use the wire type
offer (RFC-3339 strings instead of time.Time zero values). Primary transport
is HTTP (cheapo mcp --http), stdio only as fallback. The server is
stateless — deduplication ("what is new?") is the client's job,
not the server's (no seen set, no state file).
- Shared list logic: the modes free/upcoming/deals (including
capability interfaces game.UpcomingProvider/game.DealProvider and
warn-instead-of-error) live in internal/ops and are shared by CLI, MCP and
chatto-send. New list modes only there, not in cmd/ or
internal/mcpserver.
- User-facing strings in English (CLI output, help texts, error messages),
code comments in English.
- Unambiguous date outputs: prefer ISO-8601 (RFC-3339 / 2006-01-02)
for machine-readable/structured surfaces (CLI --json, MCP offer wire type);
compact locale-style formats (e.g. 02.01.) are only for human-facing chat
messages and must be overridable via CHEAPO_DATE_FORMAT. The CLI's human-
readable "Free until:" line honors CHEAPO_CLI_OUTPUT_DATE_FORMAT (default
RFC-3339). Avoid ambiguous DD.MM./MM/DD outputs without a year.
- Wrap errors with context: fmt.Errorf("epic: ...: %w", err).
- New store platforms (Prime, GOG, …) follow the provider pattern (above).
- Chatto client: internal/chatto is a hand-built Connect-JSON client
(Chatto's generated Go code is internal/ in the Chatto repo and not
importable from outside). Credentials ONLY via
CHATTO_BASE_URL/CHATTO_TOKEN env (set by the operator) — the tool never
fetches credentials itself, and the agent does not use its own plugin
credentials for it.
## Testing
- Tests use table-driven tests where possible; live fixtures for HTML/API
responses live in internal/<store>/testdata/.
- Run before every commit: go build ./... && go vet ./... && go test ./...