about_kicksmash
About Kicksmash
What Kicksmash is and how to use it: matches, levels, groups, boards, the API and what we ask of assistants. Read this once before acting.
无需输入参数。

sh.kicksma由 kicksma.sh 发布官方发布者v1.1.0更新于 Oct 24, 2026
Padel matches for people and agents: create, share one link, join, americano schedules, levels.
Kicksmash 是一个面向板式网球(padel)的远程 MCP 服务器:助手可查询比赛、俱乐部、系列赛和教练,生成美式轮转赛程,并创建或加入比赛。
Web executable,通过 Streamable HTTP。 远程服务在工作区中配置后即可从网页运行时运行。
把这段提示词粘贴到 Claude Code、Codex、Cursor 等智能体,或 SourceWeft 对话中。智能体会读取该服务器的安装指南,先向你展示发布者、端点和登录方式,经你同意后再把它添加到你的客户端。密钥不会写进任何命令。
阅读 https://sourceweft.com/mcp/sh.kicksma%2Fkicksmash/install.md,并按其中说明把这个 MCP 服务器接入我的客户端。修改任何配置之前,先告诉我它的发布者、端点和登录方式,等我确认。不要把 API 密钥写进命令或我会提交的文件;问我密钥应从哪里获取。把它添加到你客户端的 mcpServers 配置中。
{
"mcpServers": {
"kicksmash": {
"type": "http",
"url": "https://kicksma.sh/mcp"
}
}
}Mobile-first web app for organizing padel matches with zero app installs, zero accounts, zero passwords. Create a match, share kicksma.sh/{code} on WhatsApp or Telegram, friends tap → enter a name once → they're in.
/p/{token}, shown on My matches, in every email and in the calendar invite) that signs any device in; an email that was used before can restore history with a 6-digit code, merging all identities that share it. The home-screen shortcut opens the personal link, and calendar entries and emails carry the private event link (/p/{token}/{code}: signs the device in, opens the match). A newly added email receives the personal link (inside the calendar invite when in a match, otherwise on its own). Tokens are 12 characters; older 32-char tokens keep working as previous_token after the lazy shortening. An email can be changed but never blanked once set; the previous address is kept as recovery_email, so a restore code sent to either address gets the player back in. "Email me this link" on My matches mails the personal link (native share, copy and QR are the other options). Passkeys (DECIDING rule 39): after a join, "Remember me on this phone" saves a passkey in the phone's password manager, and from then on "Sign in with Face ID or fingerprint" signs any browser on that phone in, first in "Played before?", which stands open under the name field of the match page's join form. A session by name never saves one; a sign-in folds only the bare new record of the same name that this browser made. Kicksmash keeps the public key, a label and two dates (passkeys), and My matches lists "Phones that remember you" with Remove.src/lib/domain/notices.ts): it lands in their inbox on My matches first (ninety days, newest first, in the reader's language), and goes out on their channel only when that kind is on (seven kinds, all on but the club's new matches) and it is not their quiet hours, unless the match starts within three hours. What quiet hours held goes as one short message per channel when they end, from the hourly cron./api/cron/push is called every 5 minutes by Supabase pg_cron + pg_net (Vercel Hobby cron is daily). Set VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, VAPID_SUBJECT./{code} (4 chars, public) · /{code}/i/{6} (personal invite) · /{code}/manage/{10} (organizer secret)./api/openapi.json), an MCP server at /mcp that any assistant adds by URL, instant self-serve keys, signed webhooks, llms.txt, /.well-known/mcp.json, a robots.txt that welcomes AI crawlers, AGENTS.md, an installable skill (npx skills add evhg/padel-matchup), calendar feeds per group and venue. Public data is CC BY 4.0. See /developers and /agents.RESEND_API_KEY the app runs fully with email features hidden./about says what is stored, what is never done, and how to leave, in a few lines; /privacy (the one faint footer link) and /terms say it in full, with the cookies, the processors and the figures the code enforces (src/lib/legal.ts). Every organizer-initiated email (invites, invite reminders) carries a signed one-tap /unsubscribe link; a player adding their own address again lifts the opt-out. "Delete my account" at the bottom of My matches wipes personal data (name, email, phone, personal link, the linked Telegram, Discord and LINE accounts, the public page, the passkeys), releases upcoming spots, cancels the player's own upcoming matches, and — if they coach — cancels their upcoming lessons (each student told, with the coach's real name) and takes their coach page down with everything on it; old scores stay as "Deleted player". A coach who wants only the page gone, keeping the account, has "Delete my coach page" at the bottom of their settings, which refuses while a lesson is still to come.metrics_daily, no extra infrastructure. Hitting one returns "too many" and nothing else happens.That's it. With no DATABASE_URL the app boots an embedded PGlite database in ./.pglite, applies migrations and seeds two example events:
Copy .env.example to .env to change anything. All flows (join, waitlist, invites, scores, "My matches", OG previews) work end-to-end without any keys.
pnpm e2e boots next start on port 3001 with a throwaway PGlite database, a dummy Resend key (email UIs on, sends fail harmlessly) and generated VAPID keys, then runs every e2e/*.mjs suite. SHOTS=./shots keeps full-page screenshots; PW_CHROMIUM=/path/to/chromium uses a preinstalled browser. GitHub Actions runs typecheck, lint, vitest on PGlite and on a real Postgres service, the build, and the e2e suites on every push and pull request (.github/workflows/ci.yml).
Tests run on in-memory PGlite by default. To run them against a real Postgres (true concurrency), point TEST_DATABASE_URL at a disposable database — its public schema is dropped before each test file:
Only one variable is required in production: the database URL. Everything else has a safe default.
Generate secrets: openssl rand -base64 32. The table above and .env.example are written by node scripts/gen-docs.mjs from the process.env reads in the code, and tests/docs.test.ts fails when either falls behind, so a variable the code reads cannot go undocumented. Check a deployment any time at /api/health (no secrets returned).
The walkthrough lives in docs/DEPLOY.md: Supabase, Vercel, a domain, Resend and the cron jobs, in a browser-only version and a CLI version. Nothing in it is needed to run the app locally.
The pure engines ship as packages, generated from src/lib/domain so there is one source of truth:
@erikv69/americano: buildSchedule({ names | players, courts, rounds, seed }) for a whole americano, plus the round-by-round planners (planRound, planMexicanoRound, planKingRound), histories and standings.@erikv69/levels: bands, presets, ranges and levelFit, balancedTeams, matchDeltas and tournamentDeltas.pnpm packages:build regenerates packages/*/src and packages/*/dist (both gitignored); tests/packages.test.ts builds them on every CI run. To release: bump the version in packages/<name>/package.json, build, npm publish from that directory.
Questions, ideas and "I built a thing on the API" go to GitHub Discussions. Bugs go to issues. There is a Discord server too (the link is on /developers), where the bot answers questions about once an hour. The Telegram bot and the Reddit account answer people where they are; the code and the roadmap live here.
Kicksmash is one Next.js project and one Postgres database, Apache-2.0. Run it for your club, your city or your country: docs/DEPLOY.md has the Vercel button, the Docker build and the walkthrough. The environment table above is the whole configuration, and a test keeps that true.
src/lib/booking/unfurl.ts, DECIDING rule 35). It never signs in to a platform and never books or pays.UPDATE … WHERE id = (SELECT … LIMIT 1) claims exactly one slot, so two taps on the last spot resolve cleanly (tested with 12 parallel joins).data/whatsapp-templates.json), at most WHATSAPP_TEMPLATES_PER_DAY a day; a template that cannot go falls through to email, then push. No Google/Apple buttons for a single match: those create unlinked copies we could never update. A chat player's calendar is instead a feed of their own matches (/p/{key}/calendar.ics, from thirty days back, the same UID and fields as the invitation, a cancelled match marked), offered as webcal:// and through Google Calendar on a computer; the key is a hash of the personal token, so the address signs nobody in. Invitation entries carry exactly one link, the short private event link (kicksma.sh/p/{12 chars}/{code}), plus the player list once complete; feed entries carry the public match link.- COMPLETE in the title and the player names in the description (and reverted if someone drops out). The app never messages anyone; organizers get one-tap WhatsApp / Telegram forward buttons with pre-filled localized text. Declined slots become open spots.x-vercel-ip-timezone (browser zone as fallback), editable, stored UTC./new hands out a create link bound to the chat; a pasted kicksma.sh link becomes a card when the bot is admin or privacy mode is off; /match CODE works always), one short "line-up complete" note, one reminder about an hour before (via the 5-minute push cron), and the result picture once the organizer confirms. English or Russian per chat (/lang). People who tap become players with just their first name; Telegram sign-in on My matches (Login Widget) links or merges that account with the browser identity. A reply "+1" or "can't make it" to a card does what the two buttons do, and the bot answers with a reaction, not a message. Privacy mode stays on: the bot reads plain messages only in a crew's own group, one that a group admin tied to a crew with the link the crew page shows the crew's admins ("Run a Telegram group for this crew"), where the bot is an admin and its notice is pinned; there "in", "out" and "who's in Thursday 7pm Rawai?" (at a court the chat already knows) work without a tap, in English, Russian and Spanish, and /quiet from a group admin stops it acting on words. It keeps first names, Telegram usernames, who is in or out, the court and the score, never the text. Module: src/lib/telegram/./new hands out a create link bound to the channel, /match CODE posts the card, the two buttons join and leave through the shared operations and the card updates in place for everyone; one "line-up complete" note, one reminder, the result once. /ask answers a padel or Kicksmash question on the spot, and the hourly tick reads new messages in the servers the bot is in, keeps the ones that read like questions and answers them in a reply (this is our own community, so no tap is needed; the shared daily budget still applies, and every reply grows an answer page). /lang en|ru per channel. Module: src/lib/discord/./admin/listen is the desk (owner only, via Telegram sign-in). Daily ceilings on drafts and tokens keep a capped API key safe. Approved replies grow into evergreen answer pages at /answers/{slug} (question rewritten generically, QAPage JSON-LD, in the sitemap), published at once; a Sunday digest on Telegram lists the week and offers one-tap Unpublish for each new page. Module: src/lib/listen/./feedback in Telegram or Discord, the web form at /feedback, or an email to feedback@ becomes a note. The person gets an honest thank-you at once (the note was read; they hear if something gets built; nothing else is promised). The owner gets a proposal on Telegram the moment a real note arrives: the verdict the rules in docs/DECIDING.md give, what would change and where, the size (small, medium, large), a timeline estimate made without reading the code, what it needs, and a recommendation, ending with "build" or "skip". Nothing is built from a note until the owner says so, and the person hears only what shipped. A production error the store has never seen is one line to the owner too, at first sight, a few an hour at most. The note and the error are the triggers; there is no daily loop./embed/board/{slug} and /embed/match/{code} are iframe-safe views (no header, opens on kicksma.sh in a new tab, "Live from kicksma.sh" footer); the venue board shows the snippet under "Embed this board". /api/oembed?url=…&format=json is an oEmbed provider and match and board pages advertise it with <link rel="alternate" type="application/json+oembed">, so WordPress, Discourse, Ghost and Notion unfurl a pasted link into the live card. Helper: src/lib/embed.ts.Dockerfile builds a standalone image; the README's environment table is the whole configuration, and a test keeps that true. Everything is Apache-2.0; run it for your club, your city or your country.src/lib/domain/formats.ts./v/{slug}/ranking ranks a club's finalized results from the last 90 days (3 points per win, 1 per draw, 3/2/1 for tournament podiums); /phuket and /singapore do the same across a city's clubs and list the open matches there. Only players who switched on "Show me in rankings" (My matches, or one tap on a ranking page) appear. Cities: src/lib/domain/cities.ts./clubs/claim (name, the city and country anywhere in the world, booking page, website, courts, a few lines about the club). The claim is self-serve and carries its own check: the claimant says their role at the club and gives a work contact; a work email at the club's own domain is confirmed by a 6-digit code on the spot, anything else the owner checks by hand; then one Telegram tap makes the page live, and the claimant hears the answer on Telegram or by email. Nothing of a pending claim shows to anybody but its claimant. Once live, /v/{slug} shows "Book on Playtomic / MATCHi / Playbypoint …" (the platform is recognised from the link, also on every match at that club), the website, the about text, the badges, and free courts today when the club shares a feed it already has: a calendar of bookings (.ics, free = courts − overlapping bookings inside opening hours) or a JSON list of free slots, refreshed hourly. Where a club shares no feed and its booking link (or, with none, its website) is on a platform Kicksmash has a reader for (Playtomic, MATCHi and Book & Go), Kicksmash reads the free court times from the platform's public club page, three days ahead. It reads today every 15 minutes for every club a reader can read, and the next two days about once an hour. The page and the API name the platform (DECIDING rule 35: no sign-in, no booking, no payment; the player books and pays on the platform; /about says what KicksmashBot reads). A feed the club shared always wins. A free court two to six hours ahead goes once to the players who asked to play at that club on that day and around that hour, on the channel they have, with one button to the match form at that club and hour (one court a day for each want). A refused claim on a club the directory listed puts the listing back as the directory has it, under a new manage link, unless the refusal says it is not a club or a duplicate. The first ten clubs per city are founding clubs: everything stays free for them for good. /clubs lists live clubs by city; GET /api/v1/clubs?city=phuket and /api/v1/clubs/{slug} expose the same publicly; the MCP tool is find_clubs. Clubs manage their page through a private link that also shows on My matches. Modules: src/lib/domain/clubs.ts, src/lib/booking/ (platform detection, the clubs' feeds, the 15-minute read in scrape.ts and one reader per platform in adapters/)./u/{slug} (first name, level with band and the organizer-confirmed tick, played / won / win rate / podiums, the clubs they play at) and gets a signed level document at /u/{slug}/passport.json: Ed25519 over canonical JSON, public key at /.well-known/kicksmash-passport.json, verifyPassport in @erikv69/levels, 90-day expiry. No list of profiles exists. Download my data (/api/me/export) is one JSON file with everything we hold, tokens, manage links and a passkey's credential id and public key excluded (its phones show by label and dates). Level import: the level picker folds a "Have a level in another app?" row that maps Playtomic (same 0–7), 1–10 club scales and five-category systems onto 0–7 in the open; the table is on /levels. Modules: src/lib/domain/passport.ts (pure, WebCrypto), src/lib/domain/profile.ts./{code}/card: a page whose link unfurls with a generated picture (score boxes with the winning side highlighted, or the top five of the table) in WhatsApp and Telegram, the picture itself to long-press and save, share buttons, and "Organize your own match" as the way in./g/{6 chars}) with the match's settings as defaults (venue, format, capacity, level range, time zone) and its players as members; the person who taps is the admin. Anyone with the link joins (name only); joining a group's match or accepting its invite makes you a member too. On the group page's member list, in GET /api/v1/groups/{code} and in the MCP tool get_group, a visitor sees the members' first names and the count, never their levels (canSeeMemberLevels in src/lib/domain/groupAccess.ts); a match's own roster still shows the levels of its seated players, as every match page does. An admin can switch on Ask to join (the owner's decision E, 9 October 2026): a newcomer asks with an optional note, the admins hear it by Telegram, email or push, and see it on the group page, where they approve or decline; the person hears the answer, and a declined person may ask again after seven days. In such a group a seat in one of its matches is only a seat, never a membership (joinGroup takes via: self, match or admin). Any member creates the next match from the group page: the create form comes prefilled (including the next weekly slot), the match is linked back, and every other member gets an email and a push with their private link. Admins can set a weekly slot: the hourly cron creates the next match a few days ahead (lead time 1–14 days, default 5), seats the group's creator, notifies everyone, and never creates the same occurrence twice. Under the group's matches, once it has two scored matches in the last 90 days, a small season table: each member who played, by first name, with matches played, won and wins in a row (🔥 from three), most wins first. My matches lists your groups with the next match of each./v/{venue-slug} (a public page named after the venue, with spots left and level range), and the match shows an "On the … board" chip. /v/{slug}/poster is a one-page printable poster with a QR code to the board ("Scan for open padel matches at …"). The board's empty state and footer lead to the create form with the venue prefilled and the listing switched on. Slugs are ASCII-only and kept in sync when a venue is renamed; removing the venue unlists the match./americano is a public, indexable page running the same rotation engine in the browser: players (or pasted names), courts, rounds; exact rotation when the field is in fours, fair sit-outs otherwise; print stylesheet; "Run it live on Kicksmash" carries the schedule into a real tournament: the names travel with it (/?type=tournament&capacity=N&s=gen&names=…), the create form opens under "Your americano, live" and names the players it carried, and each one takes a reserved spot with an invite link the moment the tournament exists (seatNames). A pasted list is capped at 64 names, the field the engine stops at. Both generator pages carry the feedback door. sitemap.xml lists /, /play, /americano, /about, /privacy and /terms in every language, beside the city, club, coach, series and answer pages, and robots.txt lets crawlers read them; personal, manage, invite, share and card pages stay out of the index.Everything public on the site is available to programs and assistants, and everything the create form does is one call away.
Public shapes (src/lib/api/serialize.ts) carry first names and levels only; emails, phones, personal tokens and manage links never appear. Errors are { error: { code, message, hint, status } }; every hint says what to do next. Limits live in src/lib/domain/ratelimit.ts.
server.json at the repository root is the manifest for the official MCP registry (namespace io.github.evhg); publishing is one command for the repository owner: mcp-publisher login github && mcp-publisher publish. The skill in skills/kicksmash/SKILL.md is picked up by the skills index from the public repository. Smithery, Glama and mcp.so accept the remote URL https://kicksma.sh/mcp through their web forms.
/admin is a public, read-only usage page: hero figure with a health row (database, email, push, both crons, errors today), stat tiles with sparklines, meters against the free-tier ceilings (Supabase 500 MB, Resend 3,000/month and 100/day), and 7/30/90-day trend charts (growth, joins, outbound messages, errors, database size, totals). Counters live in metrics_daily (bumped by the app, snapshotted by the hourly cron); no personal data is shown. Errors are counted, never described: server actions that throw, cron steps that fail and browser crash screens (/api/client-error, rate-limited) each bump a counter, so the health row is the only alerting there is. Vercel bandwidth has no public API on Hobby, so it links to the dashboard.
Schema changes: edit the file for that domain under src/db/schema/ → pnpm db:generate → commit drizzle/ → the
Migrate workflow applies it when it reaches main (AGENTS.md rule 7). bash scripts/check-migrations.sh fails when the two disagree.
来源:README.md,提交 dc9f49a
About Kicksmash
What Kicksmash is and how to use it: matches, levels, groups, boards, the API and what we ask of assistants. Read this once before acting.
无需输入参数。
Get a match
A match or tournament by its 4-character code: players with levels, spots left, venue, time, level range, result. Public data only.
code必填Find open matches at a venue
Open, organizer-listed matches at a venue (its public board), with spots left and level ranges. Give the venue name; the slug is derived.
venue必填Get a group
A crew that plays together: members' first names and count (their levels are for members only, so null here), weekly slot, whether it asks to join, upcoming matches, calendar feed.
code必填Generate an americano schedule
Rounds of rotating-partner doubles for 4 to 64 players. Exact when the field is in fours (every pair partners once in players−1 rounds), fair sit-outs otherwise. Nothing is stored.
seednamescourtsformatroundsplayersFind clubs
Padel clubs: booking link and platform, courts, today's free courts (from the club's own calendar feed, or read from its booking platform's public page: freeCourts.source says which, and freeCourts.platform names it), founding status. Every row says `claimed`: true when the club runs the page itself, false when Kicksmash listed it from public sources, where the courts and links are our reading and may be out of date. Pass include:'listed' to get both — that is what answers "where can I play here?". Filter by city (phuket, singapore) or ask for one club by name.
citynameincludeFind series (Opens that repeat)
Tournament series: same weekday and time every week, fortnight or month, with the next edition to sign up for and the past podiums. Filter by city, or pass a slug for one series. Players sign up on the edition's match page.
cityslugFind coaches
Padel coaches who listed their page: clubs, lesson length, languages, the rules students book under (cancellation cutoff, free late passes, notice). Filter by city. Prices are the coach's to tell; nothing is paid through Kicksmash.
cityA coach's free times
Free starts inside a coach's hours for the next days (default 14). Only accepted students can book them: request_coach first.
dayshandle必填Ask to become a student
Ask a coach to accept a person as their student, by first name or personal token. The coach answers with one tap. Keep the returned personalToken private to the student; book_lesson needs it. Ask the person before calling.
nametokenhandle必填Book a lesson
Book one lesson at a free start for an accepted student (their personal token). Draws from their open package when they have one. Confirm the time with the person first.
token必填handle必填startsAt必填Cancel a lesson
Cancel a student's lesson under the coach's rules; the outcome says whether it was refunded, covered by a free pass, or counted. Say the rule to the person before cancelling late.
token必填lessonId必填Create a match
Create a padel match (4 players) or an americano tournament for a person. A tournament may keep fixed pairs (fixedPairs: true): two partners play every round together and the table ranks pairs. Returns the share link for the players and the organizer's private links. Give the person all links; keep personalToken and manageUrl private. Ask before creating; one request, one match.
tz必填costnotetypecourttitlevenueageMinformatgamesTocapacitycategory另有 13 个Join a match
Put a person into a match by first name (or by their personal token from an earlier call). Handles waitlists and level ranges; when the level is outside the range, the organizer is asked to approve, unless the player already played a finished, scored match with that organizer. On a fixed-pairs tournament pass partner (a first name) to sign up as a pair: the answer's partner.inviteUrl is the link the partner opens to claim their spot; without partner the person is listed as needing a partner. For a group's match the answer says whether the person is now a member of the group; a group that asks to join is never joined through a match. A place the organizer holds for the same name refuses the join (name_held) and writes nothing: if it is that person, they take the place on the match page; if not, they join with a fuller name, such as a last name. A name (with no token) that a player already in the match carries refuses the join (name_seated) and writes nothing: if it is that player, call again with their personal token; if not, join with a fuller name.
code必填nameemailleveltokenpartnerCreate an API key
Get a free key instantly for roomier limits and webhooks. Shown once. Not needed for reading or for a few writes a day.
name必填agentemail已满足适用检查项的 100%
该评分衡量此收录条目的完整程度,依据注册中心条目、端点与公开来源计算得出。它不是对服务器本身的评价,也不表示服务器安全或当前可用:可连通性是带日期的检查结果,评分也不包含安装量或用户评价。
pnpm installpnpm devpnpm test # vitest: slot-claim concurrency, invite transitions, score-lock rules, reminders, identity, americano rotation, rate limits, opt-outs, account deletionpnpm typecheckpnpm lintpnpm buildpnpm e2e # the sixteen Playwright journeys against a fresh production build (`node scripts/suites.mjs --why` says which a change can break); first time: pnpm exec playwright install chromiumTEST_DATABASE_URL=postgres://postgres:postgres@localhost:5432/padel_test pnpm testsrc/app/ routes: / · /[code] · /[code]/share · /[code]/card (+ opengraph-image) · /[code]/i/[invite] · /[code]/manage/[manage] · /g/[code] (+ calendar.ics) · /v/[slug] (+ /poster, /ranking, calendar.ics) · /phuket · /singapore · /clubs (+ /claim) · /v/[slug]/manage/[token] · /api/v1/clubs (+ /[slug]) · /u/[slug] (+ passport.json) · /api/me/export · /.well-known/kicksmash-passport.json · /api/telegram/{webhook,setup,login} · /api/discord/{interactions,setup} · /admin/listen · /answers (+ /[slug]) · /embed/{board,match}/… · /api/oembed · /me · /p/[token] (+ /calendar, calendar.ics) · /about · /privacy · /terms · /play · /americano · /developers · /agents · /mcp · /api/v1/* · /api/openapi.json · /llms.txt · /.well-known/mcp.json · /unsubscribe · /admin · /api/cron/{hourly,push} · /api/client-error · robots.txt · sitemap.xmlsrc/app/[code]/opengraph-image.tsx link preview (Inter w/ Cyrillic, organizer's language)src/actions/ server actions (identity incl. restore codes, events, slots, scores)src/lib/domain/ pure business logic, driver-agnostic (events, slots, scores, reminders, queries)src/lib/notify.ts every outbound email; safe no-op without RESEND_API_KEYsrc/lib/calendar.ts Google Calendar URL + RFC 5545 .ics builder (also served at /{code}/calendar.ics); inviteFields, what every entry sayssrc/lib/calendarFeed.ts a player's own feed: its key, its matches, its .icssrc/lib/domain/identity.ts personal tokens, email one-time codes, identity mergesrc/lib/domain/{ratelimit,optouts,anonymize}.ts abuse ceilings, unsubscribe list, account deletionsrc/lib/domain/{levels,requests,rating}.ts level maths (ranges, presets, balanced teams, deltas), join requests, result-based adjustmentsrc/lib/domain/groups.ts groups, membership (joinGroup with via), asks to join, weekly slots (recurrenceDue / autoCreateGroupMatches)src/lib/domain/groupAccess.ts pure: who sees members' levels, what a join does, when a declined person may ask againsrc/lib/domain/venueBoard.ts venue slugs, the public board query, listing togglesrc/lib/api/ public REST (operations, serialize, keys, webhooks, openapi), MCP server (mcp.ts), model-facing docs (docs.ts)src/lib/channels/ one card algorithm (cards.ts) with Telegram and Discord as adapters over itsrc/lib/domain/facts.ts the append-only fact log every view is a query overskills/kicksmash/SKILL.md installable skill for coding agents; AGENTS.md at the root for agents working on this reposrc/lib/domain/{levels,rating,requests}.ts level scale, presets, fit, balanced teams, Elo-style deltas; join requestssrc/lib/alerts.ts error counters for the admin health rowsrc/db/schema/ the tables, one file per domain (enums, players, events, demand, groups, clubs, coaching, channels, api, ops)src/db/ driver factory (postgres-js | PGlite), seeddrizzle/ generated SQL migrationsmessages/{en,ru,es}.json all UI, share and email copy (identical key sets, typed in global.d.ts)tests/ vitest against PGlite (or TEST_DATABASE_URL)e2e/ Playwright journeys + runner (pnpm e2e).github/workflows/ci.yml typecheck · lint · schema vs migrations · vitest (PGlite + Postgres) · build · e2e (2 shards)scripts/ gate.sh (the pre-push gate), check-migrations.sh, gen-docs.mjs (this README's env table)