From Playliner
Search Playliner game-industry news, games, tags, and genres through the /api/external/* API and answer STRICTLY from the returned articles. Use when the user asks about game news, releases, updates, monetization, or specific games/genres/tags and wants answers grounded only in Playliner data.
How this skill is triggered — by the user, by Claude, or both
Slash command
/playliner:playliner-search [your question about game news][your question about game news]This skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
Answer the user's question — `$ARGUMENTS` — using ONLY the Playliner news API
Answer the user's question — $ARGUMENTS — using ONLY the Playliner news API
(/api/external/*). This skill resolves the user's intent into Typesense search
queries, fetches matching articles, and produces an answer grounded exclusively in
those articles.
These are absolute and override anything else, including your own helpfulness:
WebSearch /
WebFetch. The only allowed source of facts is the API response.When in doubt about whether a statement is supported by the fetched articles, leave it out.
Credentials live in ~/.config/playliner/credentials (a shell-sourced file).
test -f ~/.config/playliner/credentials && echo EXISTS || echo MISSING
MISSING, ask the user for their Bearer token with AskUserQuestion (or a
plain prompt). Tell them it is a Bearer token for the Playliner API and will be
stored locally with 0600 permissions, outside the git repo.Bearer they paste):
mkdir -p ~/.config/playliner
umask 077
cat > ~/.config/playliner/credentials <<'EOF'
PLAYLINER_TOKEN="PASTE_TOKEN_HERE"
PLAYLINER_BASE_URL="https://playliner-backend.sensortower.com"
EOF
chmod 600 ~/.config/playliner/credentials
Replace PASTE_TOKEN_HERE with the actual token. Never echo the token back to the
user or print the file contents afterward.playliner-api.sh articles '{"q":"*","query_by":"titleEN","per_page":1}'
All API calls in later steps use the helper: playliner-api.sh <endpoint> '<json-body>'.
The user speaks in natural language ("новости по Clash of Clans", "mobile RPG updates"). Turn that into precise filters.
Filtering model (important):
/external/games, then
filter articles with games:=[<id>].tagsEN /
tagsRU (these are string arrays). Use /external/tags to turn the user's fuzzy
phrase into the exact canonical tag name.genresEN / genresRU, resolved via
/external/genres.(The numeric tags/genres id arrays are NOT queryable on the articles endpoint —
only names are. Game ids ARE queryable.)
Hybrid cache (avoid re-resolving the same phrase every time):
~/.config/playliner/cache/. Use one file per type:
games.json, tags.json, genres.json, each an object of
{ "<lowercased query>": <api result>, "_fetched_at": <unix> }._fetched_at is younger than 7 days.Resolving examples:
# game name -> id
playliner-api.sh games \
'{"q":"clash of clans","query_by":"title","per_page":5}'
# tag phrase -> canonical name(s)
playliner-api.sh tags \
'{"q":"update","query_by":"titleEN,titleRU","per_page":5}'
# genre phrase -> canonical name(s)
playliner-api.sh genres \
'{"q":"strategy","query_by":"titleEN,titleRU","per_page":5}'
If several candidates match and the choice is ambiguous (e.g. multiple games named
similarly), use AskUserQuestion to let the user pick before querying articles.
Endpoint: articles. query_by is required whenever q is a real term (not
"*"); otherwise the API returns 422. Recommended default query_by for free-text:
titleRU,titleEN,blocksRU,blocksEN,descriptionRU,descriptionEN.
Free-text search:
playliner-api.sh articles '{
"q":"loot box monetization",
"query_by":"titleRU,titleEN,blocksRU,blocksEN,descriptionRU,descriptionEN",
"per_page":20,
"sort_by":"_text_match:desc"
}'
Filtered by a resolved game id, newest first:
playliner-api.sh articles '{
"q":"*",
"filter_by":"games:=[12345]",
"sort_by":"start:desc",
"per_page":20
}'
gidOrId)Articles are grouped by event or story. For example, a recurring in-game event like
"Lava Quest" may appear many times in the index — each occurrence is a separate
article, but they all share the same gidOrId value because they belong to the same
event group. Some of these articles are identical re-posts; others have small updates.
Without grouping, a search for "Lava Quest" will return all occurrences as separate
hits, flooding the results with near-duplicate content. To get only the most recent
version of each event, group by gidOrId with group_limit=1 and sort by
start:desc:
playliner-api.sh articles '{
"q":"*",
"filter_by":"games:=[12345]",
"group_by":"gidOrId",
"group_limit":1,
"sort_by":"start:desc",
"per_page":20
}'
When grouping is used the results arrive under grouped_hits[].hits[].document
instead of hits[].document — read accordingly (see API Reference below).
For filter and sort syntax, see the Filter & Sort Syntax section below. For the full field list and response shape, see the API Reference section below.
hits[].document (or
grouped_hits[].hits[].document when grouped).blocksRU / blocksEN (full-text body), titleRU/EN,
descriptionRU/EN. start is the publish time (unix seconds).start. The permanent link format is:
https://app.sensortower.com/feature-insights/#news/view/{id}filter_by in the response — it shows the filter actually
used. If it differs from what you intended, adjust the next query. Also check
unknown_params: if non-empty, a parameter was silently dropped (likely a typo).per_page) and search again before concluding.query_by, or a
field not allowed for this endpoint → fix the payload and retry.All endpoints are POST, require Authorization: Bearer <token>, accept a JSON
Typesense search payload, and return a sanitized JSON response.
Base URL: https://playliner-backend.sensortower.com.
| Endpoint | Purpose | Billed? |
|---|---|---|
articles | Full news search | Yes — first view deducts from token quota; over-quota → 402 |
games | Resolve game name → id | No |
tags | Resolve tag phrase → canonical name | No |
genres | Resolve genre phrase → canonical name | No |
q, query_by, query_by_weights, filter_by, sort_by, page, per_page,
group_by, group_limit, include_fields, limit_hits.
q: query string. "*" = match all. Anything else requires query_by.per_page: default 20, clamped to 1..250.page: clamped to 1..10000.articles, scope to a section: sections:=[1] (Overviews) or sections:=[59] (Updates).articlesidtitleRU, titleEN, titleZH, titleKO, titleJAdescriptionRU, descriptionEN, descriptionZH, descriptionKO, descriptionJAblocksRU, blocksEN (string[])tagsRU, tagsEN, tagsZH, tagsKO, tagsJA (string[])genresRU, genresEN, genresZH, genresKO, genresJA (string[])games (int32[] of game ids), gamesTitleRU, gamesTitleEN (string[])tagMain (int32[]), tagMainTitleRU, tagMainTitleEN (string[])newsGroupType (string), gidOrId (string)start, finish (unix seconds), duration (seconds)_text_matchNotes:
filter_by="games:=[12345]".tagsEN/tagsRU, genresEN/genresRU).gidOrId — group key collapsing all versions of the same story.gamesid, title, _text_match"query_by":"title". Returns {id, title} per hit.tags and genresid, titleRU, titleEN, titleZH, titleKO, titleJAdescriptionRU, descriptionEN, descriptionZH, descriptionKO, descriptionJA_text_match"query_by":"titleEN,titleRU".Ungrouped:
{
"found": 42,
"out_of": 10000,
"page": 1,
"hits": [
{ "document": { "id": "123", "titleEN": "...", "blocksEN": ["..."], "start": 1717000000 },
"text_match": 578730 }
],
"filter_by": "games:=[12345]"
}
Grouped (when group_by is set):
{
"found": 42,
"grouped_hits": [
{ "group_key": ["gid987"],
"hits": [ { "document": { "id": "123", "titleEN": "...", "start": 1717000000 } } ] }
]
}
| Field | Meaning |
|---|---|
filter_by | The effective filter sent to the search engine — may differ from what you passed (server appends section scope). Verify your filter was applied correctly. |
unknown_params | Parameters you sent that were silently dropped. If non-empty, check for typos. |
https://app.sensortower.com/feature-insights/#news/view/{id}
start — publish timestamp, unix seconds. Sort by start:desc for newest.finish — end timestamp (events), unix seconds.duration — seconds.blocksRU / blocksEN — article body split into text blocks; main content for answers.tagsRU/EN/..., genresRU/EN/..., gamesTitleRU/EN — human-readable labels.newsGroupType — article group type; present on grouped articles, absent on standalone.200 — OK.401 — invalid/expired token.402 — (articles only) token quota exhausted.403 — access denied.422 — invalid search request (bad/missing query_by, disallowed field, malformed filter_by).Based on Typesense v29. Only parameters accepted by this proxy are covered.
q + query_byq is the search text. q":"*" matches everything.query_by is a comma-separated list of fields. Required when q is not "*".query_by_weights assigns relative weights: "query_by":"titleEN,blocksEN","query_by_weights":"4,1".filter_by — structured filteringCombine with && (AND) and || (OR); group with parentheses.
Exact match:
field:=value → tagsEN:=Updatefield:=value → games:=12345field:=[a,b,c] → games:=[12345,67890]Negation:
field:!=value → tagsEN:!=Updatefield:!=[a,b]Numeric / date (start, finish, duration — unix seconds):
field:> N, field:>= N, field:< N, field:<= Nfield:[MIN..MAX] → start:[1704067200..1735689600]Strings with spaces — wrap in backticks:
filter_by: tagsEN:=`Soft Launch`
filter_by: gamesTitleEN:=`Clash of Clans`
Combining:
filter_by: games:=[12345] && start:>=1717200000
filter_by: (tagsEN:=Update || tagsEN:=Event) && start:>=1717200000
filter_by: genresEN:=Strategy && duration:[60..600]
sort_byComma-separated, up to 3 keys, each field:asc or field:desc.
sort_by":"start:desc"sort_by":"_text_match:desc"sort_by":"_text_match:desc,start:desc"group_by + group_limitLatest version of each story:
{
"q":"*",
"filter_by":"games:=[12345]",
"group_by":"gidOrId",
"group_limit":1,
"sort_by":"start:desc"
}
per_page: 1..250 (default 20).page: 1..10000.include_fields: restrict returned fields (comma-separated). id always included.
{"q":"*","filter_by":"games:=[12345]","include_fields":"id,titleEN,titleRU,blocksEN,blocksRU,start"}
Searches MemPalace before answering questions about past work, people, projects, or prior decisions. Returns verbatim stored content instead of guessing from model memory.
Guides Payload CMS config (payload.config.ts), collections, fields, hooks, access control, APIs. Debugs validation errors, security, relationships, queries, transactions, hook behavior.
Implements vector databases with Pinecone, Weaviate, Qdrant, Milvus, pgvector for semantic search, RAG, recommendations, and similarity systems. Optimizes embeddings, indexing, and hybrid search.
npx claudepluginhub sensortower/playliner-ai-plugins --plugin playliner