Prompts Awesome
Prompts are the foundation of all generative AI. Discover, share, and collect high-quality prompts in the community.
or explore by industry
Click to explore
or explore by industry
Click to explore
Prompts are the foundation of all generative AI. Discover, share, and collect high-quality prompts in the community.
or explore by industry
Click to explore
Reframe a player's current situation to reveal new meaning, goals, roles, or playstyles without changing the underlying mechanics. Use when diagnosing stagna...
--- name: game-design-player-perspective-reframe description: Reframe a player's current situation to reveal new meaning, goals, roles, or playstyles without changing the underlying mechanics. Use when diagnosing stagnation, boredom, or mid/late-game disengagement; when designing re-engagement prompts, adaptive guidance, or dynamic missions; or when a player is technically able to continue but no longer sees the current state as interesting, valuable, or purposeful. --- # Game Design Player Perspective Reframe Reframe a player's current situation so the same game state can be interpreted through a more motivating lens. Use this skill when the player is not blocked by a fundamentally broken system, but by a stale interpretation of what their situation means or what kind of play is currently available to them. ## Core principle Sometimes the problem is not lack of content, but lack of meaning. A player can have options available and still feel stuck because they are reading the current state through an exhausted frame: "I cannot grow," "I am behind," "nothing is happening," or "this part is just waiting." Reframing changes the interpretation of the state so a new kind of goal, role, or challenge becomes visible. ## What to produce Generate: 1. **Current state summary** - what the player is doing, wanting, and feeling 2. **Stagnation diagnosis** - why the current frame is no longer working 3. **Reframe options** - alternative ways to interpret the current state 4. **Chosen reframe** - the strongest new lens 5. **Action hook** - immediate next objective or prompt 6. **Expected effect** - why the reframe may restore interest, agency, or momentum 7. **Use-case judgment** - whether reframing is actually the right intervention, or whether the underlying system instead needs fixing ## Process ### 1. Define the stuck state Clarify: - what the player is trying to do - what they believe is the problem - what the system state actually looks like - what kind of disengagement is happening: boredom, frustration, aimlessness, repetition, self-comparison fatigue, etc. Write: - **Player state** - **Current goal** - **Why the current frame is failing** ### 2. Decide whether reframing is appropriate at all Before generating reframes, check whether the problem is truly interpretive rather than structural. Reframing is appropriate when: - the player has meaningful options, but does not currently value or notice them - the underlying systems are basically sound, but the player's current lens is exhausted - the game can support alternate self-directed goals without pretending the state is healthier than it is - the intervention is meant to extend or redirect engagement, not conceal a broken loop Reframing is not the right primary move when: - the system is actually opaque, unfair, or under-rewarding - the player lacks real agency or feasible next steps - the economy is over-constrained and the reframe would just romanticize waiting - frustration is caused by balance, UX, matchmaking, or monetization abuse If the issue is mostly structural, say so clearly and treat any reframe as secondary at best. ### 3. Diagnose the dominant stagnation pattern Common patterns: - **growth lock** - player only values expansion and cannot see value in consolidation - **efficiency fatigue** - player is optimizing mechanically but no longer feels purpose - **goal vacuum** - no compelling next objective is visible - **identity exhaustion** - player has overidentified with one role or playstyle - **failure fixation** - player reads current state only as a deficit or loss - **content blindness** - systems are present but the player does not recognize them as meaningful play ### 4. Choose a reframe strategy Use one or combine several: #### Role reframe Shift who the player is right now. Examples: - builder -> optimizer - collector -> curator - attacker -> steward - grinder -> planner #### Goal reframe Shift what success means. Examples: - expansion -> refinement - speed -> elegance - raw power -> consistency - completion -> experimentation #### Constraint reframe Turn a limitation into a challenge premise. Examples: - "What can you achieve with only your current tools?" - "Can you solve this with one district / one deck / one weapon class?" #### System reframe Reveal another layer of meaning already present in the same mechanics. Examples: - "This is not just waiting; this is production planning." - "This is not a content gap; it is a logistics puzzle." #### Narrative reframe Wrap the current state in story meaning. Examples: - recovery phase - rebuilding chapter - proving-ground moment - specialist mission #### Social reframe Redefine the current state through comparison, contribution, or recognition. Examples: - show off efficiency - mentor others - attempt a community challenge - compare style rather than speed ### 5. Generate multiple plausible reframes Produce at least three candidate reframes before choosing one. Each candidate should include: - new interpretation - why it fits the current state - what kind of player it is most likely to help - risk of backfiring ### 6. Select the best reframe Pick the reframe most likely to: - restore agency - make the current state feel meaningful - create an immediate next step - fit the player's likely values - avoid lying about a broken system Important: do not use reframing to excuse an actually broken or abusive loop. If the system is fundamentally busted, say so. ### 7. Attach an action hook The reframe must point to a concrete next move. Examples: - optimize output using only current buildings - redesign one district around beauty instead of income - complete a self-imposed low-resource challenge - treat the next three sessions as a scouting-and-planning phase - focus on one underused system and master it Without an action hook, the reframe stays abstract and weak. ### 8. State the expected effect The expected effect should be modest and believable. Good targets: - renewed curiosity - restored short-term agency - lower self-defeating frustration - better recognition of alternate goals already present in the system - a temporary bridge from stale play to fresher play Bad targets: - masking a broken progression wall - making players accept exploitative friction - pretending a starved content phase is secretly rich ### 9. State the use-case judgment Conclude with a blunt judgment: - **Strong fit for reframing** - **Partial fit; system fixes matter more** - **Weak fit; this is mostly a structural problem** Say why. Explain what the reframe is trying to change: - restore curiosity - reduce frustration by changing success criteria - open a new playstyle identity - create a short-term challenge layer - transform waiting into anticipation or planning ## Response structure ### Current State Summary - ... ### Stagnation Diagnosis - ... ### Reframe Options 1. ... 2. ... 3. ... ### Chosen Reframe - ... ### Action Hook - ... ### Expected Effect - ... ### Use-Case Judgment - ... ## Fast mode Use this quick pass when speed matters: - what is the player currently trying to do? - is the problem interpretive or structural? - why does the current frame feel dead? - what other role, goal, or lens could fit the same state? - what should the player do immediately under that new frame? ## Working principle A good reframe does not pretend the player's situation is different. It makes a different and more useful truth visible inside the same situation.
Buy or browse Bitrefill — 1,500+ gift cards, mobile top-ups, and eSIMs across 180+ countries, payable in crypto, Lightning, USDC via x402, or pre-funded acco...
---
name: bitrefill
description: "Buy or browse Bitrefill — 1,500+ gift cards, mobile top-ups, and eSIMs across 180+ countries, payable in crypto, Lightning, USDC via x402, or pre-funded account balance. Routes the host agent to its highest-fidelity channel (residential browser, MCP server, npm CLI, or REST API) based on detected runtime capabilities, with a dedicated OpenClaw integration guide for chat-channel scenarios. Triggers when the user mentions Bitrefill, gift cards, mobile top-up, eSIM data plan, refilling a phone, or asks to pay or check out with crypto, Lightning, USDC, or x402."
compatibility: "Detects host capabilities at runtime. Paths require: browse — residential-IP browser; MCP — MCP-capable client + Bitrefill OAuth/API key; CLI — Node.js >=18 + shell + npm; API — outbound HTTP + Bitrefill API key (Personal) or API ID/Secret (Business/Affiliate). OpenClaw host gets a dedicated guide."
metadata:
author: bitrefill
version: "2.1.0"
homepage: "https://www.bitrefill.com"
docs: "https://docs.bitrefill.com"
repository: "https://github.com/bitrefill/cli"
---
# Bitrefill
Bitrefill sells digital goods (gift cards, mobile top-ups, eSIMs) across 180+ countries and 1,500+ brands. Pay with crypto, Lightning, USDC via x402, or pre-funded account balance. Codes deliver instantly after payment confirms.
This skill **routes by capability, not by use case**. Same intent ("buy a Steam card") plays out differently across hosts. Pick a path below based on what your runtime can do.
## Pick a path
Walk these checks **in order**. First match wins.
1. **Inside OpenClaw?** Check for `~/.openclaw/openclaw.json`, `~/.openclaw/skills/`, or `openclaw` on PATH. If yes → read [host-openclaw.md](references/host-openclaw.md) first. OpenClaw is a superset host: it can run all four paths plus chat-channel scenarios (Telegram purchase, cron top-up, mobile camera). After setup, return here and pick MCP/CLI/API for the actual task.
2. **Browse-only intent (no purchase)?** If the user only wants to explore, compare prices, or learn how products work:
- Have a residential-IP browser (ChatGPT Atlas, Cursor browser tool, Claude/Playwright Chrome extension, OpenClaw on user host)? → [browse.md](references/browse.md).
- Datacenter egress only (ChatGPT web/Agent, Gemini consumer, Jules)? `www.bitrefill.com` returns **403 Cloudflare** to datacenter IPs. Use [mcp.md](references/mcp.md) `search-products` / `product-details` instead — they return the same catalog without scraping.
3. **MCP supported?** Bitrefill ships a remote HTTP/SSE MCP at `https://api.bitrefill.com/mcp`. Works on Claude.ai (Pro+), Cowork, Claude Desktop, Claude Code, ChatGPT (Plus+), Atlas, Codex CLI, Gemini CLI, Cursor, OpenCode, OpenClaw. **Highest-fidelity purchase channel — typed tool calls, OAuth or API key, no shell needed.** → [mcp.md](references/mcp.md).
4. **Shell + `npm install` available?** Claude Code, Codex CLI, Cursor, Gemini CLI, OpenCode, OpenClaw, Jules (ephemeral VM), ChatGPT Agent (sandbox). → [cli.md](references/cli.md).
5. **Outbound HTTP from agent loop?** Anywhere shell exists, plus Claude Code `WebFetch`. Last resort — verbose, no typed validation. → [api.md](references/api.md).
6. **None of the above** (e.g. Gemini consumer free tier): give the user a `bitrefill.com` link and stop.
Don't know which host you're in? Read [capability-matrix.md](references/capability-matrix.md) — per-client cheat sheet maps every leading agent product to its viable paths.
## Top spending safeguards (read full list before any purchase)
This skill enables **real-money transactions**. Codes deliver instantly and digital goods are non-refundable per EU consumer rights.
- **Confirm before buying.** Present product, denomination, price, payment method. Wait for explicit user approval. Autonomous purchasing only when user opts in for the current session.
- **Treat codes as cash.** Never paste in group chats or public channels. Prefer in-memory storage over plain-text logs. Advise user to redeem ASAP.
- **Use a dedicated, low-balance account.** Never give the agent access to high-balance accounts or crypto wallet seeds. This skill is **not a wallet**.
- **Log every purchase.** `invoice_id`, product, amount, payment method.
Full safeguards + per-host hardening (OpenClaw exec-approvals, Cursor auto-approve, Codex sandbox, Claude Code allowlist) → [safeguards.md](references/safeguards.md).
## References
| File | Use when |
|------|----------|
| [browse.md](references/browse.md) | Agent has residential-IP browser; user wants to explore |
| [mcp.md](references/mcp.md) | MCP-capable host; preferred purchase path |
| [cli.md](references/cli.md) | Shell + npm available; headless scripting |
| [api.md](references/api.md) | HTTP-only runtime; Personal / Business / Affiliate REST tiers |
| [host-openclaw.md](references/host-openclaw.md) | Running inside OpenClaw Gateway |
| [capability-matrix.md](references/capability-matrix.md) | Per-client viable paths cheat sheet |
| [safeguards.md](references/safeguards.md) | Spending policy + per-host hardening |
| [troubleshooting.md](references/troubleshooting.md) | Common errors across all paths |
## Source of truth
Skill summarizes and routes. For exhaustive enums (countries, payment methods, full endpoint list), follow link-outs to <https://docs.bitrefill.com>.
FILE:references/api.md
# Path: REST API
Use when: outbound HTTP available but no MCP and no shell. Last resort — verbose, no typed validation. Examples below use `curl` but any HTTP client works.
Base URL: `https://api.bitrefill.com/v2`
## Three tiers
| Tier | Auth | Use case |
|------|------|----------|
| Personal | Bearer token | Personal projects, agent automation |
| Business | Basic auth (`API_ID:API_SECRET`) | Platforms, resellers, BRGC batches, deposits, test products |
| Affiliate | Basic auth | Same as Business + commission tracking, results filtered by `referrer_id` |
## Personal API (agent default)
Get key: <https://www.bitrefill.com/account/developers>.
```bash
export BITREFILL_API_KEY=YOUR_API_KEY
H="Authorization: Bearer $BITREFILL_API_KEY"
# 1. Ping
curl -H "$H" https://api.bitrefill.com/v2/ping
# → {"data":{"message":"pong"}}
# 2. Balance
curl -H "$H" https://api.bitrefill.com/v2/accounts/balance
# 3. Search
curl -H "$H" "https://api.bitrefill.com/v2/products/search?q=amazon"
# 4. Product details
curl -H "$H" https://api.bitrefill.com/v2/products/amazon-us
# 5. Buy (balance, instant)
curl -X POST -H "$H" -H "Content-Type: application/json" \
-d '{
"products": [{"product_id":"amazon-us","package_id":"amazon-us<&>50","quantity":1}],
"payment_method": "balance",
"auto_pay": true
}' \
https://api.bitrefill.com/v2/invoices
# 6. Order / redemption
curl -H "$H" https://api.bitrefill.com/v2/orders/{order_id}
# → data.redemption_info.code, .link, .pin, .instructions
```
For crypto: omit `auto_pay`, set `payment_method: "bitcoin"|"lightning"|"usdc_base"|...`, include `refund_address` for crypto methods, then poll `GET /invoices/{id}` until `status: "complete"`.
## Business API
Apply: <https://www.bitrefill.com/integrate>.
```bash
TOKEN=$(printf "%s:%s" "$BITREFILL_API_ID" "$BITREFILL_API_SECRET" | base64)
H="Authorization: Basic $TOKEN"
curl -H "$H" https://api.bitrefill.com/v2/ping
```
Adds: BRGC (Bitrefill Reusable Gift Card) batches, account deposits via crypto, full product catalog including test products. Same endpoints + `POST /brgc-batches`, `POST /accounts/deposit`.
## Affiliate API
Apply: <https://www.bitrefill.com/affiliate>. Same auth as Business. Adds `GET /commissions` with `after`/`before` date filters. Order/invoice queries return data filtered by `referrer_id` instead of `user_id`.
## Key endpoints
- `GET /ping` — health check (1 req / 3 s)
- `GET /accounts/balance` — current balance
- `GET /products` — paginated catalog (cache locally, refresh daily; 1000 product req/hr quota shared with search)
- `GET /products/search?q=...` — keyword search
- `GET /products/{id}` — product details with `packages` array
- `POST /invoices` — create invoice (max 20 products)
- `POST /invoices/{id}/pay` — pay unpaid balance invoice
- `GET /invoices/{id}` — status
- `GET /orders/{id}` — redemption info
- `POST /esims` — create eSIM invoice (or top-up existing via `esim_id`)
- `GET /esims` / `GET /esims/{id}` — list / get user eSIMs
Webhooks: `webhook_url` field on invoice creation → notification when delivered.
## Test products
Business/Affiliate only. No money charged. Examples: `test-gift-card-code`, etc. Full list: <https://docs.bitrefill.com/docs/test-products>.
## Rate limits
Most endpoints 60 req / 10 min. `/products` and `/products/search` 60 req/min + 1000 product req/hr quota. `/ping` 1 req / 3 s. Full table: <https://docs.bitrefill.com/docs/rate-limits>.
## Source of truth
- <https://docs.bitrefill.com/docs/api-overview> — tier comparison + auth
- <https://docs.bitrefill.com/docs/quickstart-2> — 6-step purchase flow
- <https://docs.bitrefill.com/reference> — full endpoint catalog
- <https://docs.bitrefill.com/docs/error-codes> — error codes
- <https://docs.bitrefill.com/docs/webhooks> — webhook payload spec
FILE:references/capability-matrix.md
# Capability Matrix
Per-host cheat sheet. Each entry = viable paths in priority order + one-line reason. Pick the first that fits, fall back as needed.
Legend:
- **MCP** → [mcp.md](mcp.md)
- **CLI** → [cli.md](cli.md)
- **API** → [api.md](api.md)
- **Browse** → [browse.md](browse.md) (residential IP required)
- **OpenClaw** → [host-openclaw.md](host-openclaw.md)
## Anthropic
### Claude.ai web — Free
- No MCP custom URLs (Pro+ only). No shell. No residential browser.
- **Path**: none viable for purchases. For browse: only if user installs Claude-for-Chrome extension → Browse.
- **Fallback**: send user `bitrefill.com` link.
### Claude.ai web — Pro / Max / Team / Enterprise / Cowork
- MCP custom URLs allowed. Cowork adds desktop shell.
- **Paths**: MCP first → Browse via Claude-for-Chrome ext.
- Cowork only: + CLI via desktop shell.
### Claude Desktop
- MCP first-class (stdio + remote). No native shell, no native FS, no native HTTP — wire via MCP servers.
- **Paths**: MCP first → CLI via stdio MCP wrapping `npx @bitrefill/cli` → Browse via Chrome ext or Computer Use.
### Claude Code (CLI)
- Most flexible. Full host shell, MCP, WebFetch, Chrome ext.
- **Paths**: MCP first → CLI second → API via WebFetch / curl → Browse via Chrome ext or browser-use skill.
- Tighten: sandbox allowlist `api.bitrefill.com`, `registry.npmjs.org`. Deny `~/.ssh`, `.env`.
## OpenAI
### ChatGPT web — Free
- No custom MCP, no shell, datacenter browser → Cloudflare 403.
- **Path**: none. Send user `bitrefill.com` link.
### ChatGPT web — Plus / Pro / Business / Enterprise / Edu
- Custom MCP via Apps & Connectors (Developer Mode for write tools). Code Interpreter has no network.
- **Path**: MCP only. Browser is OpenAI datacenter — **do NOT route to Browse** (Cloudflare).
### ChatGPT Desktop
- Same as ChatGPT web. "Work with Apps" can read IDE/terminal panes but not execute.
- **Path**: MCP only.
### ChatGPT Atlas
- Built-in Chromium with **residential IP** (user's network). Inherits account connectors. No shell.
- **Paths**: Browse first (its superpower) → MCP via account connectors.
### ChatGPT Agent (formerly Operator)
- Sandboxed Linux + code interpreter. Hosted browser uses **OpenAI datacenter IP**.
- **Paths**: MCP via account connectors → CLI inside sandbox shell → API via curl. **Do NOT route to Browse** (Cloudflare).
### OpenAI Codex CLI
- Full host shell (Seatbelt/Landlock sandboxable). MCP stdio + HTTP. Profiles in `config.toml`.
- **Paths**: MCP first → CLI second → API via curl. Browser via MCP only.
- Tighten: `--sandbox workspace-write --ask-for-approval on-request`. API key in profile, not committed config.
## Google
### Gemini consumer — Free
- No MCP. No shell. No residential browser.
- **Path**: none. Send user `bitrefill.com` link.
### Gemini consumer — AI Pro / Ultra (US)
- "Auto Browse" runs from Google IPs → likely Cloudflare-blocked on bitrefill.com.
- **Path**: try Auto Browse + bitrefill.com URL; if blocked, send user the link.
### Gemini CLI
- Full host shell (sandboxable: Seatbelt / Docker / gVisor). MCP stdio + SSE + streamable-http.
- **Paths**: MCP first → CLI second → API via `web_fetch` or curl. Browser via MCP (Chrome DevTools / Playwright).
### Jules (async coding agent)
- Ephemeral Ubuntu VM, Google IPs, no MCP exposed to user, no residential browser.
- **Paths**: CLI inside VM → API via curl. **Not interactive** — best for batch tasks. No purchases recommended.
## Other
### Cursor IDE
- Built-in browser tool, terminal tool, MCP (40-tool cap across servers). Cloud Agents in isolated VM.
- **Paths**: MCP first → CLI in terminal → API via shell or built-in browser → Browse via built-in browser.
- Tighten: keep `buy-products` out of `autoApprove` in `.cursor/mcp.json`.
### OpenCode (sst/opencode)
- Full host shell. MCP stdio + HTTP. Permission model per agent (`allow`/`ask`/`deny`).
- **Paths**: MCP first → CLI second → API via `webfetch` or shell. Browser via MCP.
### OpenClaw — superset host
- Agentskills.io loader. MCP via `openclaw mcp set`. Full host shell + FS. `browser` tool uses host IP. Mobile nodes (camera, canvas, voice). Cron. Multi-channel chat (Telegram, WhatsApp, Slack, Discord, iMessage, Signal, Matrix, Teams, etc.).
- **Paths**: read [host-openclaw.md](host-openclaw.md) **first** for setup + safeguards. Then MCP → CLI → API → Browse as task requires.
- Default agent: **Pi** (Anthropic / OpenAI / Google compatible via API key).
- Unique scenarios: chat-channel purchase from phone, cron auto-renew top-ups, mobile camera OCR of receipts, multi-channel handoff.
## Quick decision
If user says "what host am I in?": run `command -v openclaw` and check `~/.openclaw/`. If `command -v claude` works = Claude Code. If `command -v codex` = Codex. Look at conversation context for IDE name. When in doubt: try MCP first (broadest support), fall back to CLI, then API.
FILE:references/mcp.md
# Path: MCP
**Preferred purchase channel.** Typed tool calls, OAuth or API key, no shell, works in 10+ hosts.
## Two MCP servers
### eCommerce MCP — for purchases
URL: `https://api.bitrefill.com/mcp` (OAuth) **or** `https://api.bitrefill.com/mcp/YOUR_API_KEY` (header-less, key-in-path).
7 tools:
- `search-products` — keyword + country + category
- `product-details` — packages (denominations) + pricing
- `buy-products` — create invoice
- `get-invoice-by-id` — poll payment status
- `get-order-by-id` — get redemption info (codes, eSIM QR)
- `list-invoices` — invoice history
- `list-orders` — order history
Auth: OAuth (recommended for interactive use) or API key from <https://www.bitrefill.com/account/developers>.
### Development MCP — for docs only
URL: `https://docs.bitrefill.com/mcp`. Indexes the docs site for code-help. **Not for purchases.** Use only when authoring an integration against the Bitrefill API/CLI.
## Per-client setup
### Cursor — `.cursor/mcp.json` (project) or `~/.cursor/mcp.json` (global)
```json
{
"mcpServers": {
"bitrefill": {
"url": "https://api.bitrefill.com/mcp",
"autoApprove": [
"search-products", "product-details",
"list-invoices", "get-invoice-by-id",
"list-orders", "get-order-by-id"
]
}
}
}
```
Keep `buy-products` **out** of `autoApprove`. Cursor caps at 40 active tools across all servers.
### Claude Code
With the **bitrefill** plugin installed from this repo’s marketplace, the eCommerce MCP is auto-registered; `claude mcp add` below is for manual-only setups.
```bash
claude mcp add bitrefill --url https://api.bitrefill.com/mcp
```
Or edit `~/.claude.json`. Override output cap with `MAX_MCP_OUTPUT_TOKENS` (default 25 000).
### Claude Desktop — `claude_desktop_config.json`
macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`. Windows: `%APPDATA%\Claude\claude_desktop_config.json`.
```json
{
"mcpServers": {
"bitrefill": { "url": "https://api.bitrefill.com/mcp" }
}
}
```
### Claude.ai (web) — Pro / Max / Team / Enterprise
Settings → Connectors → Add custom connector → URL `https://api.bitrefill.com/mcp`. Free tier cannot add custom URLs.
### ChatGPT (Plus / Pro / Business / Enterprise / Edu)
Settings → Apps & Connectors → Add → URL `https://api.bitrefill.com/mcp`. Toggle **Developer Mode** to allow `buy-products` (write tool). Free tier blocked.
### Codex CLI — `~/.codex/config.toml`
```toml
[mcp_servers.bitrefill]
url = "https://api.bitrefill.com/mcp"
bearer_token_env_var = "BITREFILL_API_KEY"
```
OAuth: `codex mcp login bitrefill`.
### Gemini CLI — `~/.gemini/settings.json` (or project `.gemini/settings.json`)
```json
{
"mcpServers": {
"bitrefill": {
"url": "https://api.bitrefill.com/mcp",
"headers": { "Authorization": "Bearer BITREFILL_API_KEY" }
}
}
}
```
OAuth: `gemini mcp auth bitrefill`.
### OpenCode — `opencode.jsonc`
```jsonc
{
"mcp": {
"bitrefill": {
"url": "https://api.bitrefill.com/mcp",
"headers": { "Authorization": "Bearer BITREFILL_API_KEY" }
}
}
}
```
### OpenClaw — see [host-openclaw.md](host-openclaw.md)
```bash
openclaw mcp set bitrefill --url "https://api.bitrefill.com/mcp/$BITREFILL_API_KEY"
```
## Workflow
```
search-products → product-details → buy-products → get-invoice-by-id → get-order-by-id
```
1. **Search**: `search-products(query="Steam", country="US", product_type="giftcard")`. `country` is uppercase Alpha-2.
2. **Details**: `product-details(product_id="steam-usa", currency="USDC")`. Returns `packages` array with `package_id` in form `{product_id}<&>{value}`.
3. **Buy**: `buy-products(cart_items=[{product_id, package_id}], payment_method, return_payment_link=true)`. Max 15 items per call.
- For instant fulfillment: `payment_method: "balance"` + `auto_pay: true`.
- For agent-driven crypto: `payment_method: "usdc_base"` + `return_payment_link: true` → use `x402_payment_url`.
4. **Poll**: `get-invoice-by-id(invoice_id)`. Statuses: `unpaid` → `payment_detected` → `payment_confirmed` → `complete`.
5. **Redeem**: `get-order-by-id(order_id, include_redemption_info=true)` → returns code / link / eSIM install URL.
Confirm with user before step 3. Logging per [safeguards.md](safeguards.md).
## Caveats
- **ChatGPT** custom MCP requires Plus+; write tools require Developer Mode (admin-enabled on workspaces).
- **Cursor** 40-tool cap across all servers.
- **Claude.ai** consumer needs Pro+ for custom URLs.
- **Code-execution sandboxes** (Claude.ai analysis tool, ChatGPT Code Interpreter) have **no network egress** — they can't call MCP servers; install MCP at the chat level instead.
## Source of truth
- <https://docs.bitrefill.com/docs/ecommerce-mcp>
- <https://docs.bitrefill.com/docs/development-mcp>
- <https://docs.bitrefill.com/docs/setup-guides>
- Per-client setup: <https://docs.bitrefill.com/docs/use-with-cursor>, `/use-with-claude-chat`, `/use-with-claude-code`, `/use-with-chatgpt`
FILE:references/cli.md
# Path: CLI
Use when: shell + `npm install` available, **host has no MCP client** (the CLI talks to Bitrefill MCP under the hood). Runtimes: Claude Code, Codex CLI, Cursor terminal, Gemini CLI, OpenCode, OpenClaw, Jules (ephemeral VM), ChatGPT Agent (sandbox).
Sandboxed shells must allowlist `registry.npmjs.org` and `api.bitrefill.com`.
## Install
```bash
npm install -g @bitrefill/cli
```
**First-time setup** (validates API key against MCP, stores credentials, auto-configures OpenClaw if `~/.openclaw/openclaw.json` exists):
```bash
bitrefill init # interactive
bitrefill init --api-key $KEY --non-interactive # CI / agents
bitrefill init --openclaw # force OpenClaw integration
```
From source: `git clone https://github.com/bitrefill/cli.git && cd cli && pnpm install && pnpm build && npm link`.
## Auth
Resolution order (first match wins):
1. **`--api-key <key>`** — global flag; can appear before any subcommand.
2. **`BITREFILL_API_KEY`** — environment variable.
3. **`~/.config/bitrefill-cli/credentials.json`** — written by `bitrefill init` (mode `0600`). Overwrite or remove to change the key.
4. **OAuth** — only when no key is available **and** the session is interactive (TTY, not `CI=true`). Browser flow; state under `~/.config/bitrefill-cli/<host>.json` (e.g. `api.bitrefill.com.json`). Clear with `bitrefill logout` (OAuth only; no-op when using API key only).
Generate keys at <https://www.bitrefill.com/account/developers>.
## Global flags
Place **before** the subcommand:
- **`--api-key <key>`** — override env and stored file.
- **`--json`** — stdout is a single JSON value per run (TOON responses decoded to JSON); status and errors go to **stderr**. Use with `jq`.
- **`--no-interactive`** — skip browser OAuth and prompts; also implied when `CI=true` or stdin is not a TTY. Fails fast if no API key.
```bash
bitrefill --json search-products --query "Amazon" --per_page 1 | jq '.products[0].name'
```
## `llm-context`
Regenerates Markdown from the live MCP `tools/list` (params, JSON Schema, example `bitrefill …` and `tools/call` payloads). Use for **CLAUDE.md**, Cursor rules, or **`.github/copilot-instructions.md`**. Connection line shows `…/mcp/<API_KEY>` (redacted), safe to commit.
```bash
bitrefill llm-context -o BITREFILL-MCP.md
# or: bitrefill llm-context > BITREFILL-MCP.md
```
## OpenClaw quick-bootstrap
If OpenClaw is detected (`~/.openclaw/openclaw.json` readable) or you pass `--openclaw`, `bitrefill init` can: write `BITREFILL_API_KEY` to `~/.openclaw/.env`, merge the Bitrefill MCP server into `~/.openclaw/openclaw.json` (env-var reference, no plaintext key in JSON), and emit `~/.openclaw/skills/bitrefill/SKILL.md`. Hardening and channel setup → [host-openclaw.md](host-openclaw.md).
## Workflow
Subcommands are discovered from the remote MCP server (`bitrefill --help` after connect). Core flow:
```
search-products → get-product-details → buy-products → get-invoice-by-id
```
### 1. Search
```bash
bitrefill search-products --query "Netflix" --country US
bitrefill --json search-products --query "Netflix" --country US --per_page 5 | jq '.products'
bitrefill search-products --query "eSIM" --product_type esim --country IT
bitrefill search-products --query "*" --category games --country US
```
`--country` = uppercase Alpha-2. `--product_type` = `giftcard` or `esim` (singular). Discover categories: `--query "*"` returns a `categories` array with slugs.
### 2. Details
```bash
bitrefill get-product-details --product_id "steam-usa" --currency USDC
```
Returns `packages` array. Each entry has `package_value` — that's the `package_id` for `buy-products`. Ignore the `<&>` compound key.
Three denomination types:
- **Numeric**: `5`, `50`, `200` (pass as number).
- **Duration**: `"1 Month"`, `"12 Months"` (exact, case-sensitive).
- **Named**: `"1GB, 7 Days"`, `"PUBG New State 300 NC"` (exact, case-sensitive).
Only values from `get-product-details` accepted. Arbitrary amounts rejected.
### 3. Buy
`--cart_items` = JSON **array**, even single item. Max 15 items.
```bash
# Numeric, crypto via x402
bitrefill buy-products \
--cart_items '[{"product_id": "steam-usa", "package_id": 5}]' \
--payment_method usdc_base
# Duration, balance (instant)
bitrefill buy-products \
--cart_items '[{"product_id": "spotify-usa", "package_id": "1 Month"}]' \
--payment_method balance
# Named, eSIM
bitrefill buy-products \
--cart_items '[{"product_id": "bitrefill-esim-europe", "package_id": "1GB, 7 Days"}]' \
--payment_method usdc_base
```
Response: `invoice_id`, `payment_link`, `x402_payment_url`, `payment_info` (`address`, `paymentUri`, `altcoinPrice`).
### 4. Track / Redeem
```bash
bitrefill get-invoice-by-id --invoice_id "UUID"
bitrefill list-orders --include_redemption_info true
bitrefill get-order-by-id --order_id "ID"
```
Invoices expire after 180 minutes. Expired = create new one.
## Critical gotchas
- `--cart_items` must be **array** `[...]`, not object `{...}`. Shell quoting matters: single quotes outside, double inside.
- Use `package_value` after `<&>`, not the compound key. WRONG `"steam-usa<&>5"`. RIGHT `5`.
- Named/duration `package_id` exact and case-sensitive. WRONG `"1GB"`. RIGHT `"1GB, 7 Days"`.
- Country code uppercase Alpha-2. WRONG `us`, `USA`, `"United States"`. RIGHT `US`.
## Recommended payment methods (for agents)
`balance` (instant, no on-chain wait, natural cap) → `usdc_base` with x402 (autonomous payment via `x402_payment_url`) → `lightning`. Other crypto requires polling. Full list: `bitrefill buy-products --help`.
## Source of truth
- <https://github.com/bitrefill/cli> — full command reference, options, flags
- <https://docs.bitrefill.com/docs/crypto-payments> — payment methods
- `bitrefill llm-context` — live tool list + schemas from the MCP server
FILE:references/troubleshooting.md
# Troubleshooting
Common errors across all paths. Full enum: <https://docs.bitrefill.com/docs/error-codes> and <https://docs.bitrefill.com/docs/References>.
## Browse path
### `403 Forbidden` when fetching bitrefill.com
Cloudflare blocks datacenter IPs. Fix: switch to residential browser (ChatGPT Atlas, Cursor browser, Claude+Chrome ext, OpenClaw on user host) or pivot to MCP/CLI/API.
### Product appears in listing but not purchasable
Geolock at IP level. URL country only filters listed inventory; checkout enforces user's IP. Tell user to access from the matching country (or VPN) — but warn this may violate ToS.
## MCP path
### Tool not visible to agent
- Cursor: 40-tool cap exceeded across all servers. Disable an unused MCP server.
- ChatGPT: Developer Mode off → write tools (`buy-products`) hidden. Toggle in Settings.
- Claude.ai consumer: Free tier cannot add custom MCP URLs. Upgrade to Pro+.
- OpenClaw: `tools.deny: ["bundle-mcp"]` accidentally hiding the server, or per-agent `tools.allow` whitelist excluding it.
### `StreamableHTTPError` with HTML body
Wrong `MCP_URL` — pointing at non-Bitrefill endpoint. Unset `MCP_URL` env var or set to `https://api.bitrefill.com/mcp`.
### OAuth loop in Cursor / Claude.ai
Clear browser cookies for `bitrefill.com`, try a different browser, ensure pop-ups not blocked.
### MCP server filtered out (OpenClaw)
OpenClaw startup safety filter rejects env keys: `NODE_OPTIONS`, `PYTHONSTARTUP`, `PYTHONPATH`, `PERL5OPT`, `RUBYOPT`, `SHELLOPTS`, `PS4`. Use only standard `*_API_KEY` / `GITHUB_TOKEN` / proxy vars in MCP server `env` blocks.
### MCP output truncated
Default cap varies by host. Claude Code: `MAX_MCP_OUTPUT_TOKENS=50000` to raise. OpenClaw: `tools.toolResultMaxChars` (default 16000). Use pagination: `--per_page 25`, multiple `list-orders` calls.
## CLI path
### `cart_items` JSON shape error
```
# WRONG (object)
--cart_items '{"product_id": "steam-usa", "package_id": 5}'
# RIGHT (array)
--cart_items '[{"product_id": "steam-usa", "package_id": 5}]'
```
### `Invalid denomination 'undefined'`
Both `product_id` AND `package_id` required per item.
### `Too big: expected array to have <=15 items`
Split into multiple `buy-products` calls.
### `per_page must be less than 500`
Server limit. Use 500 max.
### `error: required option '--<name>' not specified`
Client-side validation. Add the missing option.
### "Must be one of" enum errors
| Option | Valid values | Common mistakes |
|--------|--------------|-----------------|
| `--payment_method` | `bitcoin`, `lightning`, `ethereum`, `usdc_polygon`, `usdt_polygon`, `usdc_erc20`, `usdt_erc20`, `usdc_arbitrum`, `usdc_solana`, `usdc_base`, `eth_base`, `balance` | `paypal`, `visa`, `USDC_BASE` (case-sensitive) |
| `--product_type` | `giftcard`, `esim` | `giftcards`, `gift_card`, `sim` |
| `--country` | `US`, `IT`, `BR` (uppercase Alpha-2) | `us`, `USA`, `"United States"` |
### Wrong `package_id` for named denominations
Exact, case-sensitive. WRONG `"1GB"`, `"300 nc"`. RIGHT `"1GB, 7 Days"`, `"PUBG New State 300 NC"`. Get exact strings from `get-product-details` `packages` array.
### Compound key in `package_id`
```
# WRONG
--cart_items '[{"product_id": "steam-usa", "package_id": "steam-usa<&>5"}]'
# RIGHT (value after <&>)
--cart_items '[{"product_id": "steam-usa", "package_id": 5}]'
```
### OAuth hang or auth failure
First-time fix: run `bitrefill init` (validates key, stores `~/.config/bitrefill-cli/credentials.json`).
```bash
export BITREFILL_API_KEY=YOUR_API_KEY # switch to headless
# or
bitrefill logout # clear stale OAuth state only
```
Credentials: API key in `~/.config/bitrefill-cli/credentials.json` (remove file or re-run `bitrefill init` to replace). OAuth tokens/state in `~/.config/bitrefill-cli/<host>.json` (e.g. `api.bitrefill.com.json`); cleared by `bitrefill logout`.
### Empty search results, no error
`found: 0` with no error message. Causes:
- `--category` slug doesn't exist (silent miss).
- Product not available in `--country`.
- `--in_stock true` (default) filters out-of-stock.
Fix: drop `--category`, change `--country`, or `--in_stock false`.
### Unpaid invoices missing from list
`list-invoices` defaults `--only_paid true`. Use `--only_paid false`.
## API path
### `401 Unauthorized`
- Personal: `Authorization: Bearer $BITREFILL_API_KEY` missing or wrong key.
- Business / Affiliate: `Authorization: Basic $(echo -n "$ID:$SECRET" | base64)` malformed.
### `429 Too Many Requests`
Rate limited. Defaults: 60 req / 10 min on most endpoints, 60 req/min on `/products` + `/products/search` plus 1000 product req/hr quota, 1 req / 3 s on `/ping`. Back off + retry. Cache product catalog locally.
### `RESOURCE_NOT_FOUND` on `GET /invoices/{id}`
Bad invoice ID. Verify via `list-invoices`.
### `Product '{slug}' is not available`
Bad product slug. Verify via `search-products`.
### Invoice expired
Invoices expire after **180 minutes**. Cannot re-pay. Create new one.
## OpenClaw-specific
### Cron purchase failed silently
`exec-approvals.json` set to `ask: on-miss` but no operator online to `/approve`. Either pre-approve `bitrefill buy-products` for trusted SKU/amount, or schedule when operator available.
### Pi agent can't see the Bitrefill MCP
Check:
1. `openclaw mcp list` shows entry.
2. `~/.openclaw/openclaw.json` parses (no trailing commas).
3. Agent profile not denying `bundle-mcp` or whitelisting tools narrowly.
4. `BITREFILL_API_KEY` env var set in Gateway environment, not just current shell.
### Mobile node camera tool unavailable
Node not paired or paired but offline. Check `openclaw nodes list`. Re-pair via Control UI (`openclaw dashboard`).
### Telegram message not reaching agent
`channels.telegram.dmPolicy: "pairing"` and sender not paired. Run `openclaw pairing approve telegram <CODE>` (codes expire 1 hr).
## Source of truth
- Bitrefill error codes: <https://docs.bitrefill.com/docs/error-codes>
- Bitrefill error handling: <https://docs.bitrefill.com/docs/References>
- Rate limits: <https://docs.bitrefill.com/docs/rate-limits>
- OpenClaw troubleshooting: <https://docs.openclaw.ai/help> + per-tool pages
FILE:references/host-openclaw.md
# Host: OpenClaw
[OpenClaw](https://docs.openclaw.ai/) is a self-hosted Gateway that bridges chat apps (Telegram, WhatsApp, Slack, Discord, iMessage, Signal, Matrix, Teams, etc.) to coding agents like **Pi**. It is a **superset host**: full host shell, agentskills.io-compatible skill loader, first-class MCP, mobile-node camera/canvas, cron, and multi-channel routing.
This file explains how to install + harden the Bitrefill skill inside OpenClaw and lists scenarios no other host can do. After setup, use the regular path files for the actual workflow.
## 1. Detect OpenClaw
Check **any** of:
- File: `~/.openclaw/openclaw.json` exists.
- Dir: `~/.openclaw/skills/` exists.
- Binary: `command -v openclaw` succeeds.
- Tools in agent loop: `gateway`, `cron`, `nodes`, `canvas`, `sessions_*` (OpenClaw-only).
If yes → continue here. Otherwise → return to [SKILL.md](../SKILL.md) and pick a path.
## 2. Install this skill
Loader paths (increasing precedence): `skills.load.extraDirs` → bundled → `~/.openclaw/skills/` → `~/.agents/skills/` → `<workspace>/.agents/skills/` → `<workspace>/skills/`.
Manual:
```bash
cp -r path/to/bitrefill ~/.openclaw/skills/bitrefill
openclaw skills list # verify
openclaw gateway restart # or /new in chat
```
ClawHub (if/when published):
```bash
openclaw skills install bitrefill
openclaw skills update --all
```
Skill is **agentskills.io-compatible** — no rewriting needed. Source: <https://docs.openclaw.ai/tools/skills.md>.
## 3. Install Bitrefill MCP (preferred path)
CLI:
```bash
openclaw mcp set bitrefill --url "https://api.bitrefill.com/mcp/$BITREFILL_API_KEY"
```
Or hand-edit `~/.openclaw/openclaw.json`:
```json
{
"mcp": {
"servers": {
"bitrefill": {
"url": "https://api.bitrefill.com/mcp",
"headers": { "Authorization": "Bearer BITREFILL_API_KEY" }
}
}
}
}
```
Transport: SSE/HTTP (default for URL entries) or `transport: "streamable-http"`. The 7 Bitrefill MCP tools surface as ordinary Pi tool calls. Restrict per-agent via `agents.list[].tools.allow`/`deny` if running multi-agent. Source: <https://docs.openclaw.ai/cli/mcp.md>.
Then: see [mcp.md](mcp.md) for tool workflow.
## 4. Install Bitrefill CLI (fallback)
Pi has first-class `exec` tool running on the Gateway host (sandboxing **off** by default).
```bash
exec: npm install -g @bitrefill/cli
```
If Gateway runs in Docker sandbox: declare `setupCommand: "npm install -g @bitrefill/cli"` and ensure `network` is not `none`. Source: <https://docs.openclaw.ai/gateway/sandboxing.md>.
Then: see [cli.md](cli.md).
## 5. Raw API path
`exec` + `curl`, or built-in `web_fetch` tool. No special config. See [api.md](api.md).
## 6. Browser
Pi has `browser` tool. **It uses the Gateway host's IP** — usually residential when Gateway runs on user's machine, but a VPS will hit Cloudflare 403. For richer DOM control attach a Playwright/Chrome MCP. The Mac menubar app drives user's actual Chrome and is fully residential. See [browse.md](browse.md).
## 7. OpenClaw-only scenarios
These are the differentiators. None of the other hosts can do them.
### Buy a gift card from Telegram (away from desk)
User DMs the bot: "buy a $50 Steam US card for me". Pi routes to Bitrefill MCP, prompts confirmation in chat, pays from `balance`, returns redemption code.
**Risk**: redemption codes are cash-like. Never deliver to group chats or via `MEDIA:` URLs. Lock down channel:
```jsonc
{
"channels": {
"telegram": {
"botToken": "TELEGRAM_BOT_TOKEN",
"dmPolicy": "pairing",
"allowFrom": ["123456789"],
"groups": { "*": { "requireMention": true } }
}
}
}
```
Source: <https://docs.openclaw.ai/channels/telegram>.
### Auto-renew mobile top-up monthly
Use `cron` tool to schedule `buy-products` for a fixed phone-top-up SKU on the 1st of each month, paying from `balance`. Heartbeat (default 30 min) polls `get-invoice-by-id` until `complete` then pings the user.
### Multi-channel handoff
Trigger purchase from Slack, deliver redemption code only to user's private Signal DM. Same Gateway, isolated session per channel/sender.
### Mobile camera context
Paired iOS/Android node exposes `camera.snap` and `canvas.*`. User photographs a recipient's request ("100 EUR Decathlon France"), Pi OCRs/parses, runs `search-products` + `buy-products`. Source: <https://docs.openclaw.ai/nodes/index.md>.
### Heartbeat-driven invoice polling
Default 30-min heartbeat or custom `cron` polls `get-invoice-by-id` until `status: complete`, then pushes redemption code to originating channel.
## 8. OpenClaw-specific safeguards
OpenClaw defaults are permissive: sandboxing off, `security: full`, `ask: off`. **Tighten before letting an agent buy on your behalf.**
- **Restrict who triggers purchases**: `channels.<ch>.allowFrom: ["<your_id>"]` + `dmPolicy: "pairing"`. Same for WhatsApp, Signal, Slack, Discord.
- **Require approval for buys**: `~/.openclaw/exec-approvals.json` with `security: allowlist` + `ask: on-miss`. Allowlist `bitrefill *` for read tools; force `/approve` for `bitrefill buy-products` and the MCP `buy-products` call.
- **Isolate Bitrefill agent**: under `agents.list[]` declare a Bitrefill-scoped persona with `tools.deny: ["gateway"]` so the agent **cannot rewrite Gateway config** to bypass approvals. Source: <https://docs.openclaw.ai/tools/exec-approvals.md>.
- **Pre-fund only `balance`** with low cap. **Never** give the agent crypto wallet seeds. Skill is not a wallet.
- **No voice readback of codes**: disable `audio_as_voice` / TTS for the Bitrefill agent. Pi's media pipeline could otherwise speak a cash-like code aloud over Telegram voice notes.
- **No `MEDIA:<url>` for redemption codes**: enforce text-only delivery for the redemption tool output.
## Source of truth
- OpenClaw docs: <https://docs.openclaw.ai/>
- Skills loader: <https://docs.openclaw.ai/tools/skills.md>
- Creating skills: <https://docs.openclaw.ai/tools/creating-skills.md>
- MCP CLI: <https://docs.openclaw.ai/cli/mcp.md>
- Exec tool: <https://docs.openclaw.ai/tools/exec.md>
- Sandboxing: <https://docs.openclaw.ai/gateway/sandboxing.md>
- Exec approvals: <https://docs.openclaw.ai/tools/exec-approvals.md>
- Nodes: <https://docs.openclaw.ai/nodes/index.md>
- Channels: <https://docs.openclaw.ai/channels/telegram>
- Bitrefill skill paths: [mcp.md](mcp.md), [cli.md](cli.md), [api.md](api.md), [browse.md](browse.md), [safeguards.md](safeguards.md)
FILE:references/browse.md
# Path: Browse the Website
Use when: user wants to **explore** Bitrefill (compare prices, learn product types, check denominations, see country availability) AND your runtime has a **residential-IP browser**. Browse-only by default — for purchases prefer [mcp.md](mcp.md).
## Hard requirement: residential IP
`www.bitrefill.com` sits behind Cloudflare. **Datacenter egress = 403.** Do NOT use Firecrawl, raw `fetch`, `curl`, or any scraping API.
Viable runtimes:
- **ChatGPT Atlas** — built-in residential Chromium.
- **Cursor** — built-in browser tool runs from user's machine.
- **Claude Code / Desktop / Cowork + Claude-for-Chrome** extension drives local Chrome.
- **Any host + Playwright/Chrome MCP** running on user's machine.
- **OpenClaw Gateway on user's host** — `browser` tool uses host IP. (See [host-openclaw.md](host-openclaw.md).)
Not viable: ChatGPT web/Agent (OpenAI datacenter), Gemini consumer (Google datacenter), Jules (Google VM), any cloud sandbox without residential proxy.
## URL patterns
First path segment = **country** (Alpha-2 lowercase). Second = **language**.
- Gift cards listing: `https://www.bitrefill.com/{country}/{lang}/gift-cards/`
- Gift card category: `https://www.bitrefill.com/{country}/{lang}/gift-cards/{category-slug}/` (e.g. `/us/en/gift-cards/food/`)
- Gift card product: `https://www.bitrefill.com/{country}/{lang}/gift-cards/{product-slug}/`
- Direct search: `https://www.bitrefill.com/{country}/{lang}/gift-cards/?q={query}` (covers gift cards + top-ups + eSIMs; in-country prioritized)
- Mobile top-ups: `https://www.bitrefill.com/refill/`
- eSIMs (locale): `https://www.bitrefill.com/{country}/{lang}/esims/`
- eSIMs (browse all destinations): `https://www.bitrefill.com/esim/all-destinations`
- Single eSIM: `https://www.bitrefill.com/{country}/{lang}/esims/bitrefill-esim-{destination-slug}/` (e.g. `bitrefill-esim-japan`, `bitrefill-esim-global`)
- Auth (no locale prefix): `/login`, `/signup`
## Country in URL vs geolock
- **URL country** filters which inventory is **listed**.
- **Geolock** is enforced at **IP level** at checkout. A product may appear in listing but be unpurchasable if user's IP is outside allowed region.
Match URL country to recipient's country to surface usable cards.
## Listing filters & sort (gift cards)
Query params on any gift-card listing (`/{country}/{lang}/gift-cards/[category/]`):
- `redemptionMethod` — `online` | `instore`
- `minRating` — `2` | `3` | `4` | `5`
- `minRewards` — `1`–`10` (cashback %)
- `s` — sort: `2` = A–Z, `3` = recently added, `4` = cashback. Default = popularity.
Example: `https://www.bitrefill.com/us/en/gift-cards/food/?minRating=5&minRewards=4&redemptionMethod=instore`
## Categories (popular slugs)
`top-products`, `retail`, `apparel`, `electronics`, `food`, `restaurants`, `food-delivery`, `streaming`, `games`, `travel`, `flights`, `accommodation`, `entertainment`, `gasoline`, `vpn`, `multi-brand`, `digital-wallet`, `groceries`, `pharmacy`, `experiences`, `gifts`. Full list: <https://docs.bitrefill.com/docs/Products>.
## Suggested flow
1. Clarify product type (gift card / top-up / eSIM) + country (+ carrier for top-ups).
2. Send user to direct search URL or category path.
3. For top-ups: country → carrier → amount.
4. For eSIMs: destination → data + duration.
5. Remind user to check denomination matches recipient's needs and that geolock applies at checkout.
## Purchase from the browser?
Possible but slow and risky. Anti-bot may block agent on brand redemption sites. Prefer [mcp.md](mcp.md) or [cli.md](cli.md) for purchases. If browser checkout is the only option, follow [safeguards.md](safeguards.md) — confirm with user, log invoice ID, treat redemption code as cash.
## Source of truth
- <https://www.bitrefill.com>
- <https://help.bitrefill.com>
- <https://docs.bitrefill.com/docs/Products>
FILE:references/safeguards.md
# Spending Safeguards
This skill enables **real-money transactions**. Purchases are fulfilled instantly after payment confirms. Digital codes are non-refundable per EU consumer rights once delivered.
This page is the **agent-policy layer** — not in upstream Bitrefill or host docs. Read fully before any purchase tool call.
## Universal rules
- **Default: always confirm before purchasing.** Present product, denomination, price, payment method. Wait for explicit user approval. Autonomous purchasing only when user explicitly opts in for the current session.
- **Codes are cash-like.** A gift card code or eSIM QR is bearer money. Store securely. Never share publicly.
- **Prefer in-memory storage.** Don't write codes to plain-text logs, transcripts, or unencrypted files. Programmatically read code → use it → discard.
- **If user asks for the code**: return it but advise to (a) store securely, (b) not share, (c) redeem ASAP.
- **Dedicated, low-balance account.** Never give the agent access to high-balance accounts. Pre-fund only what the agent may spend in the current session.
- **Not a wallet.** This skill does not store private keys or manage crypto wallets. Never give the agent seed phrases, hardware-wallet PINs, or signing keys.
- **Log every purchase.** `invoice_id`, product slug, amount, payment method, timestamp.
- **Refunds**: digital goods refundable only if they don't work as expected (defective code). EU 14-day change-of-mind does **not** apply.
- **Browser redemption fallback.** If trying to redeem on a brand site triggers anti-bot, ask the user to complete redemption manually and return the code.
Terms: <https://www.bitrefill.com/terms/>.
## Per-host hardening
### OpenClaw
Defaults are permissive (sandboxing off, `security: full`, `ask: off`). Tighten:
- `channels.<ch>.allowFrom: ["<your_id>"]` + `dmPolicy: "pairing"` on every channel.
- `~/.openclaw/exec-approvals.json`: `security: allowlist` + `ask: on-miss`. Allowlist read tools (`bitrefill search-products`, `bitrefill list-*`, `bitrefill get-*`). Force `/approve` for `bitrefill buy-products` and the MCP `buy-products` call.
- `agents.list[]` Bitrefill persona with `tools.deny: ["gateway"]` so the agent cannot rewrite Gateway config.
- Disable voice readback (`audio_as_voice` / TTS) for the Bitrefill agent. Codes spoken aloud over voice notes leak.
- Force text-only delivery — no `MEDIA:<url>` for redemption code output.
Full detail in [host-openclaw.md](host-openclaw.md) §8.
### Cursor
`.cursor/mcp.json` `autoApprove` may include read tools. **Never** include `buy-products`:
```json
{
"mcpServers": {
"bitrefill": {
"url": "https://api.bitrefill.com/mcp",
"autoApprove": [
"search-products", "product-details",
"list-invoices", "get-invoice-by-id",
"list-orders", "get-order-by-id"
]
}
}
}
```
### Codex CLI
Run with sandbox + approval:
```bash
codex --sandbox workspace-write --ask-for-approval on-request
```
Put `BITREFILL_API_KEY` in a profile (`~/.codex/config.toml` `[profiles.bitrefill]`), not in committed config.
### Claude Code
In `~/.claude/settings.json` (or project `.claude/settings.json`):
```json
{
"sandbox": {
"filesystem": {
"denyRead": ["~/.ssh", ".env", "*.pem", "**/.bitrefill_token"],
"denyWrite": ["~/.ssh", ".env"]
},
"network": {
"allow": ["api.bitrefill.com", "registry.npmjs.org"]
}
}
}
```
### Claude Desktop / Claude.ai web
Per-tool approval prompts on by default. Keep them on. Don't whitelist `buy-products`.
### ChatGPT (web / Desktop / Atlas / Agent)
Developer Mode required for write tools. Keep it **off** unless actively purchasing. Confirm in-chat before every `buy-products`.
### Gemini CLI
Run with `--sandbox` (Seatbelt / Docker / gVisor). Per-shell command confirmation prompts on by default.
### OpenCode
Set permissions per agent:
```jsonc
{
"agents": {
"bitrefill": {
"permissions": {
"edit": "ask",
"bash": { "*": "ask", "bitrefill list-*": "allow", "bitrefill get-*": "allow" },
"webfetch": "ask"
}
}
}
}
```
## Payment method risk
- `balance` — instant, capped by pre-funded amount. **Lowest blast radius.**
- `usdc_base` via x402 — autonomous payment from agent-controlled wallet. Bound the wallet balance.
- `lightning` — fast, low fee. Manual pay or Lightning-capable agent.
- Other on-chain crypto — slow, requires polling. Higher chance of expired invoices (180 min).
Default recommendation: pre-fund `balance` with low cap → use `payment_method: "balance"` + `auto_pay: true`.
## What to NEVER do
- Pass redemption codes through group chats, public channels, screen-shared sessions, or shared documents.
- Speak codes aloud via TTS / voice notes.
- Store codes in version control, even private repos.
- Give the agent seed phrases or hardware-wallet PINs.
- Auto-approve `buy-products` in any host's MCP config.
- Run the Bitrefill skill from an account with stored payment cards or high balances.
## Source of truth
- Bitrefill ToS: <https://www.bitrefill.com/terms/>
- Refund policy: <https://docs.bitrefill.com/docs/refunds>
- Path setup: [mcp.md](mcp.md), [cli.md](cli.md), [api.md](api.md), [browse.md](browse.md)
- OpenClaw hardening: [host-openclaw.md](host-openclaw.md)
Interact with the SalesBlink for cold email and sales outreach automation. Use when the user needs to send cold emails, manage email lists, sequences, templa...
--- name: cold-email-salesblink description: > Interact with the SalesBlink for cold email and sales outreach automation. Use when the user needs to send cold emails, manage email lists, sequences, templates, senders, leads, inbox replies, or campaign analytics via the SalesBlink API. Also use for bulk contact imports, workspace management, or deliverability testing via HTTP requests. compatibility: > Requires network access to run.salesblink.io and a SalesBlink API key. Supports any HTTP client (curl, Node.js fetch, Python requests, PowerShell, etc.). --- # SalesBlink Public REST API v1.0.0 ## When to use this skill Use this skill when the user wants to: - Create, update, or manage email lists, sequences, templates, or senders - Add, update, move, or remove contacts/leads - Send or reply to emails via the inbox - Check campaign analytics (opens, clicks, replies, sent) - Set up outreach campaigns end-to-end - Manage workspaces, users, folders, or deliverability tests - Make any HTTP request to `run.salesblink.io/api/public/v1.0.0` ## Gotchas - **ID types matter**: Templates and contact archive use MongoDB ObjectId (24-char hex). All other entities use UUID v4. - **messageId** is the RFC822 Message-ID (e.g. `<[email protected]>`) or Microsoft Graph ID. **Crucial:** Always URL-encode this ID when using it as a path parameter (e.g. in `/inbox/:messageId/thread`). This is distinct from the internal UUID `id`. - **`senders` is a comma-separated string**, not an array. It can mix sender IDs and folder IDs — the server auto-detects each. - **Sequence `steps` fully replace on PATCH**. Send the complete desired array. - **Verification flags are IRREVERSIBLE**: `verification`, `archive_invalid`, `archive_risky` on lists can only be turned ON, never OFF. - **Sequences default to paused**: If `paused` is omitted on create, it defaults to `true`. - **`launchTimingMode: "now"` starts in 5 minutes**, not instantly. - **Template attachments use FormData field `attachment`** (not `attachments`). Max 3 per template. - **Remove template attachments via `remove_attachments`** array of file **names**. - **Adding SMTP sender requires `from_email`**, not `email`. - **If an endpoint for a specific task is not mentioned then tell the user that the endpoint is not available** - **If user does not have a list, ask them for a CSV file, or list of lead emails with data.** - **If email sender is not connected, help them connect one using APIs.** - **When asked to create a sequence or campaign for cold email outreach, first ask them about their ICP, Offer, and other details.** ## Base URL `https://run.salesblink.io/api/public/v1.0.0` ## Authentication Ask the user for their SalesBlink API key: `https://run.salesblink.io/account/integration/api` Pass it in every request as the `Authorization` header (no "Bearer" prefix): **Header:** `Authorization: YOUR_API_KEY` ## Rate Limits | Method | Limit | Window | | ------------- | ----- | ---------- | | GET | 30 | per minute | | POST / PATCH | 15 | per minute | | PUT (archive) | 10 | per minute | On `429 Too Many Requests`: wait at least 60 seconds before retrying. For batch operations, insert a 4-second delay between requests. ## Pagination Most list endpoints use `limit` (max 100) and `skip`. Activity endpoints (`/sent`, `/opens`, `/clicks`, `/replies`) use `per_page` (max 100) and `page` (1-indexed). Always paginate. Never assume a single request returns all data. ## Endpoint Categories Read the relevant reference file before performing operations in that domain: - **Lists & contacts/leads** → [references/lists.md](references/lists.md) and [references/contacts.md](references/contacts.md) - Use these endpoints when the user wants to fetch or manage lists that contain leads/contacts. A list is a container for contacts/leads. Each contact/lead contains fields like Email, First_Name, Last_Name, Phone, Company, Title, and custom fields. Contacts are added to lists in batches (up to 500 per request), can be moved between lists, updated, or removed. - **Email templates** → [references/templates.md](references/templates.md) - Use these endpoints when the user wants to create or manage reusable email templates. A template has a name, subject_line, and HTML content that supports merge variables like {{first_name}} and {{company}}. Templates can have up to 3 attachments and are referenced by sequences when building outreach steps. - **Sequences & email campaigns** → [references/sequences.md](references/sequences.md) - Use these endpoints when the user wants to create or manage automated email campaigns (sequences). A sequence connects lists (who to email), senders (which accounts send), and templates (what to send) into a timed step-by-step workflow. Steps alternate between email sends and delay periods. Sequences can be launched, paused, resumed, cloned, or archived. - **Senders & OAuth** → [references/senders.md](references/senders.md) - Use these endpoints when the user wants to connect or manage email sending accounts. A sender is an email account (SMTP/IMAP or OAuth-connected Gmail/Outlook) that sends emails on behalf of sequences. Multiple senders can be assigned to a sequence. Senders can also be organized into folders. - **Inbox & replies** → [references/inbox.md](references/inbox.md) - Use these endpoints when the user wants to view or interact with email conversations. The inbox contains reply threads, sent emails, scheduled emails, and drafts. Each thread has a messageId. The user can reply to a lead's email, mark messages as read/unread, or classify outcomes. - **Activity tracking** → [references/activity.md](references/activity.md) - Use these endpoints when the user wants to query engagement events. The system tracks four event types: sent (emails sent), opens (emails opened), clicks (links clicked), and replies (responses received). Events can be filtered by sequence, recipient email, and date range. - **Users & workspaces** → [references/organization.md](references/organization.md) - Use these endpoints when the user wants to manage team membership or workspaces. A workspace is an account boundary. Users have roles (client, user, admin, developer). Only owners and admins can invite users or create workspaces. - **Folders** → [references/folders.md](references/folders.md) - Use these endpoints when the user wants to organize resources into folders. Folders have a type (list, template, sequence, or email-sender) and group related resources together for easier management. - **Domains, signatures & warmup links** → [references/account-config.md](references/account-config.md) - Use these endpoints when the user wants to view account-level configuration. Custom tracking domains are used for click tracking in emails. Signatures are appended to outgoing emails. Warmup links are used in email warmup processes. - **Reports** → [references/reports.md](references/reports.md) - Use these endpoints when the user wants to fetch aggregated activity reports over a date range. Reports combine data across campaigns into summary views. - **Inbox placement tests** → [references/inbox-placement.md](references/inbox-placement.md) - Use these endpoints when the user wants to test email deliverability. An inbox placement test sends a test email to seed email addresses across providers (Gmail, Outlook, etc.) and reports whether the email landed in inbox, spam, promotions, or other tabs. Tests can be one-time or recurring. - **End-to-end workflow examples** → [references/workflows.md](references/workflows.md) - Use this reference when the user wants to set up a complete outreach campaign from scratch. It shows the full chain: create list → add contacts → create templates → fetch senders → create sequence → launch. ## Error Handling Always check the `success` boolean in the response body. A `200` status can still return `{ success: false, message: "..." }`. | Status | Meaning | Action | | ------ | ------------ | ----------------------------------------------------- | | 200 | Success | Check `success` field | | 400 | Bad request | Re-check payload structure against the reference file | | 401 | Unauthorized | Verify API key | | 403 | Forbidden | Insufficient permissions (role too low) | | 404 | Not found | Verify the ID / endpoint | | 409 | Conflict | Resource already exists or connection failed | | 429 | Rate limited | Wait 60s, then retry | | 500 | Server error | Retry once after 10s | FILE:references/workflows.md # Workflow Examples ## End-to-End Campaign Setup Goal: Build a complete outreach campaign from scratch. ### Step 1 — Create a list **POST** `/lists` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "name": "Q1 Prospects", "removeDuplicates": { "inThisList": true, "inOtherLists": true } } ``` Extract `LIST_ID` from the response. ### Step 2 — Add leads to the list **POST** `/contacts` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "list_id": "LIST_ID_HERE", "contacts": [ { "Email": "[email protected]", "First_Name": "Alice", "Company": "Corp Inc" }, { "Email": "[email protected]", "First_Name": "Bob", "Company": "Startup IO" } ], "remove_duplicates": true } ``` ### Step 3 — Create email templates **POST** `/templates` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "name": "Intro Email", "subject_line": "Quick question, {{first_name}}", "content": "<p>Hi {{first_name}},</p><p>I noticed {{company}} is scaling fast...</p>" } ``` Extract `TEMPLATE_1_ID` (MongoDB ObjectId). Repeat for follow-up templates. ### Step 4 — Fetch available senders **GET** `/senders` Headers: - `Authorization`: `YOUR_API_KEY` Extract target `SENDER_ID` (UUID). ### Step 5 — Create the sequence **POST** `/sequences` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "name": "Q1 Outbound Campaign", "senders": "SENDER_ID_HERE", "lists": ["LIST_ID_HERE"], "steps": [ { "type": "email", "template_id": "TEMPLATE_1_ID" }, { "type": "delay", "days": 3 }, { "type": "email", "template_id": "TEMPLATE_2_ID" } ], "paused": false, "launchTimingMode": "now" } ``` ## Clone and Modify Workflow Goal: Duplicate an existing sequence and tweak it. ### 1. Clone the sequence (creates paused copy) **POST** `/sequences/:id/clone` Headers: - `Authorization`: `YOUR_API_KEY` Extract `NEW_SEQ_ID` from response. ### 2. Update the cloned sequence with new settings **PATCH** `/sequences/:id` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "name": "Q1 Campaign — Variant B", "steps": [ { "type": "email", "template_id": "NEW_TEMPLATE_ID" }, { "type": "delay", "days": 5 }, { "type": "email", "template_id": "FOLLOW_UP_TEMPLATE_ID" } ] } ``` ### 3. Launch when ready **PATCH** `/sequences/:id` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "paused": false, "launchTimingMode": "now" } ``` ## Archive Cleanup Workflow Goal: Clean up old campaigns, lists, and templates. ### Archive a sequence (pauses it and removes pending tasks) **PUT** `/sequences/:id/archive` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "archived": true } ``` ### Archive the associated list **PUT** `/lists/:id/archive` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "archived": true } ``` ### Archive templates **PUT** `/templates/:id/archive` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "archived": true } ``` > Archiving a sequence automatically pauses it and removes pending email tasks. Archiving a list pauses it if active. Archiving a contact removes its pending tasks from all sequences. FILE:references/reports.md # Reports ## Endpoints | Endpoint | Method | Description | |----------|--------|-------------| | `/reports` | GET | Retrieve activity reports with aggregated data | ## Get Reports **GET** `/reports` Headers: - `Authorization`: `YOUR_API_KEY` Query params: | Param | Type | Description | |-------|------|-------------| | `from` | integer | Start date timestamp (milliseconds) | | `to` | integer | End date timestamp (milliseconds) | | `limit` | integer | Max 100 (default enforced server-side) | | `skip` | integer | Offset | > The endpoint maps `from`/`to` to a date range filter internally. Both are optional; omitting them returns all available report data. **GET** `/reports?from=1715000000000&to=1717600000000` Headers: - `Authorization`: `YOUR_API_KEY` FILE:references/inbox.md # Inbox & Outreach ## Endpoints | Endpoint | Method | Description | | -------------------------- | ------ | ----------------------------------------------- | | `/inbox` | GET | Retrieve inbox threads | | `/inbox/:messageId/thread` | GET | Get all messages in a specific thread | | `/inbox/:messageId/reply` | POST | Reply to a lead's email | | `/inbox/:messageId` | PATCH | Mark as read/unread, set outcome classification | ## Get Inbox **GET** `/inbox` Headers: - `Authorization`: `YOUR_API_KEY` Query params: | Param | Type | Description | | ---------- | ------- | --------------------------------------------------------- | | `type` | string | `all` (replies, default), `draft`, `scheduled`, or `sent` | | `limit` | integer | Max 100 (default: 10) | | `skip` | integer | Offset (default: 0) | | `sequence` | string | Filter by sequence UUID | | `outcome` | string | Filter by outcome classification | | `search` | string | Search in body, subject, or email address | | `date` | string | Date range as `startTimestamp-endTimestamp` | | `sender` | string | Filter by sender ID | | `owned_by` | string | Filter by user email (owners/admins only) | ``` GET /inbox?type=all&limit=50&skip=0 GET /inbox?type=sent&sequence=SEQ_ID&limit=100 GET /inbox?search=acme&limit=20 ``` Response includes `data.result` (thread array), `totalCount`, `count`, and `messageIDs`. ## Get Thread **GET** `/inbox/:messageId/thread` Headers: - `Authorization`: `YOUR_API_KEY` Returns all messages in a conversation thread, sorted newest first. ## Reply to Email **POST** `/inbox/:messageId/reply` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "content": "<p>Thanks for getting back to me! Let's schedule a call.</p>", "cc": "[email protected]" } ``` | Field | Type | Req | Description | | ------------------ | ------- | --- | ------------------------------------------------------- | | `content` | string | ✅ | HTML content of the reply | | `cc` | string | | Optional CC email address | | `bcc` | string | | Optional BCC email address | | `scheduled_time` | integer | | Schedule at this timestamp (ms). Defaults to ~20s delay | | `tzMode` | string | | Timezone mode: `"sequence"` or `"custom"` | | `selectedTimezone` | string | | Timezone identifier if tzMode is custom | > Attachments are supported via FormData field `attachment`. Base64 images in HTML are automatically uploaded to S3. > The reply is automatically sent from the same sender that originally contacted the lead. ## Update Mail State **PATCH** `/inbox/:messageId` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "unread": false, "outcome": "interested" } ``` | Field | Type | Description | | --------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `unread` | boolean | Mark as read (`false`) or unread (`true`) | | `outcome` | string | Classify the reply: `"interested"`, `"not-interested"`, `"automatic-response"`, `"meeting-request"`, `"out-of-office"`, `"do-not-contact"`, `"wrong-person"`, `"closed"` | FILE:references/senders.md # Email Senders & OAuth ## Endpoints | Endpoint | Method | Description | |----------|--------|-------------| | `/senders` | GET | List all connected senders (grouped by folder) | | `/senders` | POST | Add a single SMTP/IMAP sender | | `/senders/bulk` | POST | Bulk add senders via CSV upload | | `/oauth/google` | POST | Get Google OAuth URL for connecting Gmail | | `/oauth/outlook` | POST | Get Microsoft OAuth URL for connecting Outlook | ## Get Senders **GET** `/senders` Headers: - `Authorization`: `YOUR_API_KEY` Query params: `limit` (max 100), `skip`, `owned_by` ## Add Single Sender (SMTP/IMAP) **POST** `/senders` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "from_email": "[email protected]", "from_name": "Sales Team", "password": "your_password", "smtp_host": "smtp.yourprovider.com", "smtp_port": 587, "user_name": "[email protected]", "imap_host": "imap.yourprovider.com", "imap_port": 993 } ``` | Field | Type | Req | Description | |-------|------|-----|-------------| | `from_email` | string | ✅ | Sender email address | | `password` | string | ✅ | SMTP/IMAP password | | `smtp_host` | string | ✅ | SMTP server hostname | | `smtp_port` | integer/string | ✅ | SMTP port (e.g., 587) | | `from_name` | string | | Display name | | `user_name` | string | | SMTP username (defaults to `from_email`) | | `imap_host` | string | | IMAP hostname (omit for SMTP-only) | | `imap_port` | integer/string | | IMAP port (e.g., 993) | | `imap_user_name` | string | | IMAP username if different from SMTP | | `imap_password` | string | | IMAP password if different from SMTP | | `total_warmup_per_day` | integer | | Warmup emails per day (default: 5) | | `warmup_enabled` | boolean | | Enable warmup (default: false) | | `inbox_enable` | boolean | | Enable inbox (default: false) | | `warmup_tag` | string | | Warmup keyword/tag | | `inbox_path` | string | | Inbox folder path (default: "INBOX") | | `spam_path` | string | | Spam folder path | | `signature_id` | string | | Signature ID or name to attach | | `custom_tracking_url` | string | | Custom tracking domain (must be verified) | | `sequence_auto_ramp_up_enabled` | boolean | | Enable sequence ramp-up | | `sequence_initial_daily_frequency` | integer | | Initial daily send limit (default: 30) | | `sequence_ramp_up_frequency` | integer | | Ramp-up increment (default: 3) | | `max_emails_per_day` | integer | | Max daily send limit (default: 30) | | `dkim_identifier` | string | | DKIM identifier | | `reply_to_email` | string | | Reply-to email address | > If `imap_host` is omitted or empty, the sender is created as **SMTP-only** (`serviceName: "smtponly"`). ## Bulk Add Senders **POST** `/senders/bulk` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `multipart/form-data` Upload a CSV file via FormData with field name `csvFile`. ## Google OAuth **POST** `/oauth/google` Headers: - `Authorization`: `YOUR_API_KEY` Returns an `auth_url` that the user must visit to authorize Gmail access. Response: ```json { "success": true, "data": { "auth_url": "https://accounts.google.com/o/oauth2/v2/auth?..." } } ``` ## Outlook OAuth **POST** `/oauth/outlook` Headers: - `Authorization`: `YOUR_API_KEY` Returns an `auth_url` for Microsoft Outlook authorization. FILE:references/templates.md # Email Templates ## Endpoints | Endpoint | Method | Description | |----------|--------|-------------| | `/templates` | GET | List all templates (includes `cold_email_score`) | | `/templates/:id` | GET | Get full template details + score breakdown | | `/templates` | POST | Create a template | | `/templates/:id` | PATCH | Update template content, attachments | | `/templates/:id/archive` | PUT | Archive or unarchive a template | ## Create Template **POST** `/templates` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "name": "Cold Outreach V1", "subject_line": "Quick question, {{first_name}}", "content": "<p>Hi {{first_name}},</p><p>I noticed {{company}} is scaling fast...</p>", "folder": "folder_id", "starred": false } ``` | Field | Type | Req | Description | |-------|------|-----|-------------| | `name` | string | ✅ | Template name | | `subject_line` | string | ✅ | Email subject (supports `{{variables}}`) | | `content` | string | ✅ | HTML body (supports `{{first_name}}`, `{{company}}`, etc.) | | `folder` | string | | Folder ID (UUID) | | `starred` | boolean | | Star the template | | `attachments` | file[] | | Files to attach (**max 3 total**). Use FormData field name `attachment` | Response includes `cold_email_score`: ```json { "score": 72.5, "rating": "Good", "details": { "word_count": 85, "personalization_count": 3, "link_count": 1, "image_count": 0, "question_count": 1, "spam_word_count": 0 } } ``` ## Update Template **PATCH** `/templates/:id` (ObjectId) Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "name": "Updated Template", "subject_line": "New subject {{first_name}}", "content": "<p>Updated content here</p>", "remove_attachments": ["old_file.pdf"] } ``` | Field | Type | Description | |-------|------|-------------| | `name` | string | New template name | | `subject_line` | string | New subject line | | `content` | string | New HTML body | | `starred` | boolean | Star/unstar | | `attachments` | file[] | New files to **append** (total must not exceed 3). Use FormData field name `attachment` | | `remove_attachments` | string[] | Names of existing attachments to **remove** | > **IMPORTANT**: To remove attachments, use the `remove_attachments` array with file **names** — not the `attachments` field. ## Archive Template **PUT** `/templates/:id/archive` (ObjectId) Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "archived": true } ``` FILE:references/inbox-placement.md # Inbox Placement Tests ## Endpoints | Endpoint | Method | Description | |----------|--------|-------------| | `/inbox-placement` | GET | List deliverability tests | | `/inbox-placement` | POST | Create a new deliverability test | | `/inbox-placement/:id/pause` | PUT | Pause an active **recurring** test | | `/inbox-placement/:id` | DELETE | Delete a test | ## Get Tests **GET** `/inbox-placement` Headers: - `Authorization`: `YOUR_API_KEY` Query params: | Param | Type | Description | |-------|------|-------------| | `search` | string | Filter by test name (case-insensitive) | | `status` | string | Filter by status: `pending`, `running`, `completed`, `stopped` | | `mode` | string | Filter by mode: `one-time`, `recurring` | | `ownedBy` | string | Filter by user ID | | `limit` | integer | Page size (default: 10) | | `skip` | integer | Offset (default: 0) | | `sortBy` | string | Sort field (default: `created_at`) | | `sortType` | integer | `-1` for descending, `1` for ascending | > Note: filtering by `status=completed` excludes recurring tests since they never truly complete. ## Create Test **POST** `/inbox-placement` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` | Field | Type | Req | Description | |-------|------|-----|-------------| | `name` | string | ✅ | Test name (min 3 characters) | | `mode` | string | ✅ | `"one-time"` or `"recurring"` | | `source` | string | ✅ | `"from-salesblink"` or `"from-outside"` | | `sender_id` | string | ✅* | Sender UUID (required when `source="from-salesblink"` or `mode="recurring"`) | | `content_type` | string | ✅* | `"custom"`, `"sequence"`, or `"template"` (required when `source="from-salesblink"` or `mode="recurring"`) | | `subject` | string | ✅* | Email subject (required when `content_type="custom"`) | | `body` | string | ✅* | Email HTML body (required when `content_type="custom"`) | | `sequence_id` | string | ✅* | Sequence UUID (required when `content_type="sequence"`) | | `template_id` | string | ✅* | Template ObjectId (required when `content_type="sequence"` or `"template"`) | | `schedule_day` | integer | ✅* | Day of week: `0`=Sunday through `6`=Saturday (required when `mode="recurring"`) | | `tracking_uuid` | string | | Optional UUID for `from-outside` tests. Auto-generated if omitted. | > *Field requirement depends on `source`, `mode`, and `content_type` values. **Behavior:** - **One-time tests** run ~2 minutes after creation by default. - **Recurring tests** run weekly on the specified `schedule_day` at 09:00 UTC. - **`from-outside` tests** return `seed_emails` in the response — these are the addresses the user must send to. - **`from-salesblink` tests** send automatically using the selected sender. **V1 Wrapper behavior:** When `source="from-salesblink"` is provided without `content_type`: - If `subject` and `body` are present → `content_type` becomes `"custom"` - Otherwise → `content_type` becomes `"sequence"` with `sequence_id: "from_api"` ### Example: One-time custom content test **POST** `/inbox-placement` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "name": "Gmail Deliverability Check", "mode": "one-time", "source": "from-salesblink", "sender_id": "sender-uuid-here", "content_type": "custom", "subject": "Hello from SalesBlink", "body": "<p>This is a test email.</p>" } ``` ### Example: Recurring sequence-based test **POST** `/inbox-placement` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "name": "Weekly Sequence Check", "mode": "recurring", "source": "from-salesblink", "sender_id": "sender-uuid-here", "content_type": "sequence", "sequence_id": "sequence-uuid-here", "template_id": "507f1f77bcf86cd799439011", "schedule_day": 1 } ``` ### Example: From-outside test (returns seed emails) **POST** `/inbox-placement` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "name": "External Send Test", "mode": "one-time", "source": "from-outside" } ``` Response for `from-outside`: ```json { "success": true, "data": { "id": "...", "tracking_uuid": "...", ... }, "seed_emails": ["[email protected]", "[email protected]", ...] } ``` ## Pause Test **PUT** `/inbox-placement/:id/pause` Headers: - `Authorization`: `YOUR_API_KEY` Only works on **recurring** tests. Sets status to `stopped` and clears scheduling. ## Delete Test **DELETE** `/inbox-placement/:id` Headers: - `Authorization`: `YOUR_API_KEY` Deletes the test and all associated tracking tasks. FILE:references/folders.md # Folders ## Endpoints | Endpoint | Method | Description | | ---------- | ------ | --------------- | | `/folders` | GET | List folders | | `/folders` | POST | Create a folder | ## Get Folders **GET** `/folders` Headers: - `Authorization`: `YOUR_API_KEY` ## Create Folder **POST** `/folders` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "name": "Q1 Campaigns", "type": "sequence" } ``` | Field | Type | Req | Description | | ------ | ------ | --- | --------------------------------------------------------- | | `name` | string | ✅ | Folder name | | `type` | string | ✅ | `"list"`, `"template"`, `"sequence"`, or `"email-sender"` | > If `type` contains `"sender"`, it is automatically converted to `"email-sender"`. FILE:references/contacts.md # Contacts & Leads ## Endpoints | Endpoint | Method | Description | |----------|--------|-------------| | `/lists/:id/leads` | GET | Get leads in a list (paginated) | | `/contacts` | POST | Add up to 500 leads to a list | | `/contacts/remove` | POST | Remove a single lead by email from a list | | `/leads/:id` | PATCH | Update lead fields | | `/leads/:id/move` | PUT | Move a lead to a different list | | `/contacts/:id/archive` | PUT | Archive or unarchive a contact | ## Get Leads **GET** `/lists/:id/leads?limit=100&skip=0` Headers: - `Authorization`: `YOUR_API_KEY` Query params: `limit` (max 100), `skip` ## Add Contacts **POST** `/contacts` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "list_id": "a1b2c3d4-e5f6-7890-abcd-abcdef123456", "contacts": [ { "Email": "[email protected]", "First_Name": "John", "Last_Name": "Doe", "Phone": "+1234567890", "Company": "Acme Inc", "Title": "VP Sales", "Custom_Field": "any value" } ], "remove_duplicates": true } ``` | Field | Type | Req | Description | |-------|------|-----|-------------| | `list_id` | string | ✅ | List UUID to add leads to | | `contacts` | object[] | ✅ | Array of lead objects (**max 500 per request**) | | `remove_duplicates` | boolean | | Remove duplicate emails after insert | Each contact object: | Field | Type | Req | Description | |-------|------|-----|-------------| | `Email` | string | ✅ | Lead's email address | | `First_Name` | string | | First name | | `Last_Name` | string | | Last name | | `Phone` | string | | Phone number | | `Company` | string | | Company name | | `Title` | string | | Job title | | _(any key)_ | string | | Custom fields are supported | > **Field naming**: Use **PascalCase with underscores** (`First_Name`, `Last_Name`, `Email`). ## Remove Contact **POST** `/contacts/remove` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "list_id": "a1b2c3d4-e5f6-7890-abcd-abcdef123456", "email": "[email protected]" } ``` | Field | Type | Req | Description | |-------|------|-----|-------------| | `list_id` | string | ✅ | List UUID | | `email` | string | ✅ | Email address of the lead to remove | ## Update Lead **PATCH** `/leads/:id` (UUID) Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "First_Name": "Updated", "Last_Name": "Name", "Title": "CTO" } ``` Any standard or custom contact fields can be updated. System fields (`_id`, `id`, `list_id`, `account_id`, `user_id`, `accuracy`, `provider`, `custom_fields`, `removed_sequences`, `verification_required`, `archive_invalid_contacts`, `archive_risky_contacts`, `processing`, `completed`, `completedAt`, `last_modified`, `created_date`, `verification_blocked`, `didOpen`, `didClick`, `didReply`, `contactStats`, `retryCount`, `esg_name`, `archived`, `deleted`) **cannot** be modified. If updating `Email`, it is automatically lowercased. ## Move Lead **PUT** `/leads/:id/move` (UUID) Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "list_id": "destination_list_uuid" } ``` | Field | Type | Req | Description | |-------|------|-----|-------------| | `list_id` | string | ✅ | Destination list UUID | ## Archive Contact **PUT** `/contacts/:id/archive` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "archived": true } ``` > ⚠️ **The `:id` here is a MongoDB ObjectId** (24-char hex), NOT a UUID. This is the only contact endpoint that uses ObjectId. FILE:references/lists.md # Lists ## Endpoints | Endpoint | Method | Description | |----------|--------|-------------| | `/lists` | GET | Retrieve all lists. Query: `limit` (max 100), `skip`, `owned_by` | | `/lists/:id` | GET | Get a specific list by UUID | | `/lists/:id/leads` | GET | Get leads in a list. Query: `limit` (max 100), `skip` | | `/lists` | POST | Create a new list | | `/lists/:id` | PATCH | Update a list | | `/lists/:id/archive` | PUT | Archive or unarchive a list | ## Create List **POST** `/lists` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "name": "Q1 Prospects", "removeDuplicates": { "inThisList": true, "inOtherLists": true } } ``` Required fields marked with ✅: | Field | Type | Req | Description | |-------|------|-----|-------------| | `name` | string | ✅ | List name | | `folder` | string | | Folder ID (UUID) | | `starred` | boolean | | Star the list (default: false) | | `verification` | boolean | | Enable email verification ⚠️ **IRREVERSIBLE** | | `archive_invalid` | boolean | | Auto-archive invalid emails ⚠️ **IRREVERSIBLE** | | `archive_risky` | boolean | | Auto-archive risky emails ⚠️ **IRREVERSIBLE** | | `removeDuplicates.inThisList` | boolean | | Remove duplicate emails within this list | | `removeDuplicates.inOtherLists` | boolean | | Remove contacts that exist in other lists | | `removeDuplicates.inTeamMembersLists` | boolean | | Remove contacts that exist in team members' lists | ## Update List **PATCH** `/lists/:id` (UUID) Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "name": "Q2 Prospects Restructured", "starred": true, "archive_invalid": true } ``` | Field | Type | Description | |-------|------|-------------| | `name` | string | New name | | `starred` | boolean | Star or unstar | | `duplicate_removal` | boolean | Remove duplicates from this list | | `duplicate_removal_other_list` | boolean | Remove contacts in other lists | | `duplicate_removal_team_list` | boolean | Remove contacts in team members' lists | | `verification` | boolean | Enable verification ⚠️ **IRREVERSIBLE** | | `archive_invalid` | boolean | Archive invalid emails ⚠️ **IRREVERSIBLE** | | `archive_risky` | boolean | Archive risky emails ⚠️ **IRREVERSIBLE** | > Use `PUT /lists/:id/archive` for archiving — not this endpoint. ## Archive List **PUT** `/lists/:id/archive` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "archived": true } ``` Set `"archived": false` to unarchive. FILE:references/sequences.md # Sequences ## Endpoints | Endpoint | Method | Description | |----------|--------|-------------| | `/sequences` | GET | List all sequences. Query: `limit`, `skip`, `owned_by` | | `/sequences/:id` | GET | Get sequence details including steps and settings | | `/sequences/:id/stats` | GET | Get performance analytics. Query: `from`, `to`, `sender` | | `/sequences` | POST | Create a full sequence with steps, senders, lists | | `/sequences/:id` | PATCH | Update settings, pause/resume, rewrite steps | | `/sequences/:id/clone` | POST | Duplicate an existing sequence (created paused) | | `/sequences/:id/archive` | PUT | Archive or unarchive a sequence | ## Create Sequence **POST** `/sequences` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "name": "Q1 Outbound Campaign", "senders": "a1b2c3d4-e5f6-7890-abcd-000000000001,a1b2c3d4-e5f6-7890-abcd-000000000002", "lists": ["b2c3d4e5-f6a7-8901-bcde-000000000001"], "steps": [ { "type": "email", "template_id": "507f1f77bcf86cd799439011" }, { "type": "delay", "days": 3 }, { "type": "email", "template_id": "507f1f77bcf86cd799439012" } ], "paused": false, "launchTimingMode": "now", "timezone": "America/New_York", "delayEnabled": true, "delayFrom": 10, "delayTo": 20, "stopWhenReplyRecieved": true, "stopWhenReplyRecievedWhen": "contact" } ``` Required fields marked with ✅: | Field | Type | Req | Description | |-------|------|-----|-------------| | `name` | string | ✅ | Sequence name | | `senders` | string | ✅ | **Comma-separated string** of sender/folder IDs (NOT an array) | | `lists` | string[] | ✅ | Array of list UUIDs | | `steps` | Step[] | ✅ | Ordered array of email and delay steps | | `folder` | string | | Folder ID (UUID) | | `starred` | boolean | | Star the sequence | | `paused` | boolean | | Create in paused state (default: **true**) | | `launchTimingMode` | string | | `"now"` (starts in 5 mins) or `"schedule"` (requires `scheduledAt`) | | `scheduledAt` | integer | | UTC timestamp in **milliseconds** (required if mode = `"schedule"`) | | `timezone` | string | | Timezone for sending (default: `"America/New_York"`) | | `delayEnabled` | boolean | | Enable random delay between emails (default: true) | | `delayFrom` | integer | | Minimum delay in minutes (default: 10) | | `delayTo` | integer | | Maximum delay in minutes (default: 20) | | `stopWhenReplyRecieved` | boolean | | Stop sequence when lead replies (default: true) | | `stopWhenReplyRecievedWhen` | string | | `"contact"` or `"contact-with-same-domain"` (default: `"contact"`) | | `evergreen` | boolean | | Enable evergreen mode — continuously running (default: false) | | `bounceThreshold` | integer | | Bounces before pausing (default: 2) | | `bouncePause` | boolean | | Pause sequence on bounce threshold (default: false) | | `autoPause` | boolean | | Auto-pause on high bounce rate (default: true) | | `autoTagReplies` | boolean | | Auto-tag reply outcomes (default: false) | | `plainText` | boolean | | Send as plain text email (default: true) | | `auto_reply` | boolean | | Enable auto-reply detection (default: true) | | `matchProvider` | boolean | | Match sender email provider with recipient (default: true) | | `skip_esg` | boolean | | Skip ESG detection (default: true) | | `sendToOnlyVerifiedEmail` | boolean | | Only send to verified emails (default: false) | | `validEmail` | boolean | | Send to contacts with valid email status (default: true) | | `riskyEmail` | boolean | | Send to contacts with risky email status (default: true) | | `invalidEmail` | boolean | | Send to contacts with invalid email status (default: true) | | `checkEmailOpen` | boolean | | Check if recipient opened previous email before sending next (default: false) | | `checkEmailClick` | boolean | | Check if recipient clicked link before sending next (default: false) | | `checkEmailReply` | boolean | | Check if recipient replied before sending next (default: true) | | `checkEmailBeforeSending` | boolean | | Verify email before sending (default: true) | | `bcc` | string | | BCC email address for all outgoing emails (default: `""`) | | `emailSendingHours` | array | | Sending hours per day of the week (see default below) | **Steps structure** — ordered array mixing `email` and `delay` types: ```json "steps": [ { "type": "email", "template_id": "507f1f77bcf86cd799439011" }, { "type": "delay", "days": 3 }, { "type": "email", "template_id": "507f1f77bcf86cd799439012" } ] ``` - `type: "email"` → MUST include `template_id` (the template's MongoDB ObjectId). - `type: "delay"` → MUST include `days` (integer, number of days to wait). - Omitting `type` or misspelling it will fail or create a broken sequence. **Default `emailSendingHours`:** ```json [ { "enabled": true, "name": "Monday", "fromTime": "09:00", "toTime": "17:00" }, { "enabled": true, "name": "Tuesday", "fromTime": "09:00", "toTime": "17:00" }, { "enabled": true, "name": "Wednesday", "fromTime": "09:00", "toTime": "17:00" }, { "enabled": true, "name": "Thursday", "fromTime": "09:00", "toTime": "17:00" }, { "enabled": true, "name": "Friday", "fromTime": "09:00", "toTime": "17:00" }, { "enabled": false, "name": "Saturday", "fromTime": "09:00", "toTime": "17:00" }, { "enabled": false, "name": "Sunday", "fromTime": "09:00", "toTime": "17:00" } ] ``` ## Update Sequence **PATCH** `/sequences/:id` (UUID) Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "paused": true } ``` Accepts the same fields as create (all optional). Additionally used to **pause/resume**. > **Critical**: When updating `steps`, the entire array is **replaced** — send the full desired step list. > Updates to `name`, `starred`, or `folder` do **not** trigger task rescheduling. All other field updates do. ## Clone Sequence **POST** `/sequences/:id/clone` (UUID) Headers: - `Authorization`: `YOUR_API_KEY` Creates a paused duplicate of an existing sequence. ## Archive Sequence **PUT** `/sequences/:id/archive` (UUID) Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "archived": true } ``` Archiving a sequence automatically pauses it and removes pending email tasks. FILE:references/activity.md # Activity Tracking ## Endpoints | Endpoint | Method | Description | |----------|--------|-------------| | `/sent` | GET | Log of all sent emails | | `/opens` | GET | Email open events | | `/clicks` | GET | Link click events | | `/replies` | GET | Reply events | ## Query Parameters All activity endpoints support: | Param | Type | Description | |-------|------|-------------| | `per_page` | integer | Max 100 | | `page` | integer | 1-indexed | | `sequence_id` | string | Filter by sequence UUID | | `recipient_email_address` | string | Filter by email address | | `since` | integer | Filter events after this timestamp (ms) | | `from` | integer | Start of date range (timestamp, ms) | | `to` | integer | End of date range (timestamp, ms) | > Use `per_page` and `page` for activity endpoints — not `limit`/`skip`. ## Response Format Each event includes: ```json { "id": "...", "time": 1715000000000, "message": "Sent", "type": "outreach", "sequence": "sequence-uuid", "email": "[email protected]", "sequence_name": "Campaign Name" } ``` For clicks and replies, `template_name` is also included. ## Examples **GET** `/opens?sequence_id=SEQ_ID&per_page=100&page=1` Headers: - `Authorization`: `YOUR_API_KEY` **GET** `/replies?since=TIMESTAMP_30_DAYS_AGO&per_page=100` Headers: - `Authorization`: `YOUR_API_KEY` FILE:references/account-config.md # Account Config — Domains, Signatures & Warmup Links ## Domains **GET** `/domains` Headers: - `Authorization`: `YOUR_API_KEY` List custom tracking domains for the account. ## Signatures **GET** `/signatures` Headers: - `Authorization`: `YOUR_API_KEY` List email signatures. > Signature IDs can be referenced when adding senders via the `signature_id` field. You can pass either the signature ID or its name. ## Warmup Links **GET** `/warmup-links` Headers: - `Authorization`: `YOUR_API_KEY` List warmup link configurations. FILE:references/organization.md # Organization — Users & Workspaces ## Users | Endpoint | Method | Description | |----------|--------|-------------| | `/users` | GET | List workspace users | | `/users` | POST | Invite a user | | `/users/:id` | PATCH | Update user name or role | ### Create User **POST** `/users` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "email": "[email protected]", "role": "user", "url": "https://run.salesblink.io/dashboard" } ``` | Field | Type | Req | Description | |-------|------|-----|-------------| | `email` | string | ✅ | Email address of the new user | | `role` | string | | `"client"`, `"user"`, `"admin"`, or `"developer"`. Default: `"user"` | | `url` | string | | Optional redirect URL after accepting invitation | > Only **owners and admins** can add users. Returns 403 otherwise. ### Update User **PATCH** `/users/:id` (UUID) Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "name": "Updated Name", "role": "admin" } ``` | Field | Type | Description | |-------|------|-------------| | `name` | string | New display name | | `role` | string | One of: `client`, `user`, `admin`, `developer` | --- ## Workspaces | Endpoint | Method | Description | |----------|--------|-------------| | `/workspaces` | GET | List all accessible workspaces | | `/workspaces` | POST | Create a new workspace | | `/workspaces/:id` | PATCH | Update workspace name | > **Owner only.** Returns 403 for non-owners. ### Create Workspace **POST** `/workspaces` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "name": "Sales Team" } ``` | Field | Type | Req | Description | |-------|------|-----|-------------| | `name` | string | ✅ | Workspace name (min 4 characters) | ### Update Workspace **PATCH** `/workspaces/:id` (UUID) Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "name": "New Workspace Name" } ``` | Field | Type | Req | Description | |-------|------|-----|-------------| | `name` | string | ✅ | New workspace name (min 4 characters) |
Infer a player's underlying values and motivational priorities from behavior, then translate those into design implications. Use when designing personalizati...
--- name: game-design-player-values-mapper description: Infer a player's underlying values and motivational priorities from behavior, then translate those into design implications. Use when designing personalization, segmentation, dynamic guidance, live-ops targeting, adaptive missions, re-engagement strategies, or feature prioritization; when behavior suggests that what players actually care about differs from what the design assumes; or when a team needs a behavior-first player profile rather than a demographic or archetype-only model. --- # Game Design Player Values Mapper Map observed player behavior to likely underlying value priorities, then use that map to infer what kinds of goals, rewards, content, or framing are most likely to resonate. Use this skill when the team needs to understand not just what players do, but what those choices imply about what they care about. ## Core principle Behavior is not random. It is preference made visible. Players reveal their values through repetition, avoidance, investment, and attention. The goal is not to assign a rigid personality label, but to infer the motivational structure most likely driving current behavior and use that to improve design alignment. ## What to produce Generate: 1. **Observed behavior summary** - what the player consistently does, ignores, and invests in 2. **Value map** - likely dominant, secondary, and weak values 3. **Confidence notes** - how strong or ambiguous each inference is 4. **Tensions or contradictions** - where behavior suggests mixed motives or blocked values 5. **Design implications** - what systems, content, messaging, goals, or monetization surfaces are likely aligned or misaligned 6. **Segment hypothesis** - what kind of player pattern this most resembles in practical design terms 7. **Recommendations** - what to emphasize, reframe, personalize, or stop pushing ## Value framework Map behavior to these value dimensions: - **Efficiency / Optimization** - **Progression / Growth** - **Aesthetics / Expression** - **Collection / Completion** - **Social Recognition / Status** - **Experimentation / Discovery** - **Narrative / Meaning** You may add a clearly justified extra value if the case demands it, but do not bloat the framework casually. ## Process ### 1. Gather behavior signals List concrete observed behaviors. Possible sources: - build patterns - resource spending - session frequency and duration - event participation - feature engagement - purchase behavior - social behavior - what the player returns to repeatedly - what the player ignores despite obvious rewards Write: - **Repeated behaviors** - **Avoided behaviors** - **Investment patterns** ### 2. Map behaviors to likely value signals Translate behavior into value hypotheses. Examples: - min-maxing production chains -> Efficiency / Optimization - constant upgrading and rushing unlocks -> Progression / Growth - decorating, styling, curating loadouts -> Aesthetics / Expression - chasing every item or badge -> Collection / Completion - caring about ranks, cosmetics, visibility -> Social Recognition / Status - trying odd builds or niche tools -> Experimentation / Discovery - following lore, theme, faction identity, story arcs -> Narrative / Meaning Important: many behaviors can map to more than one value. Do not overclaim certainty. ### 3. Weight the value profile Do not force fake precision. The goal is a useful profile, not pseudo-scientific certainty. Assign rough weight levels such as: - High - Medium - Low Or if needed: - Dominant - Secondary - Weak - Absent Also note confidence: - high confidence - medium confidence - low confidence Use this format: | Value | Weight | Confidence | Evidence | |---|---|---|---| | ... | ... | ... | ... | ### 4. Detect tensions and blocked values Look for contradictions. Examples: - optimization-driven player engaging with decoration only because progression forces it - status-seeking player avoiding competition because the failure cost feels humiliating - progression-oriented player not spending because they distrust the offer structure - discovery-oriented player repeating safe loops because experimentation is too punished Ask: - is this a real mixed-value profile? - or is one value being blocked by system design? ### 5. Infer likely design alignment Answer: - what currently motivates this player most? - what kinds of content or objectives will likely land well? - what incentives are probably weak for this player? - where is the game asking for a value the player does not strongly hold? - what part of the experience is likely causing silent disengagement? - what messaging, reward framing, or mission framing is most likely to resonate? ### 6. Form a practical segment hypothesis Translate the value map into a practical design-facing player pattern. Examples: - efficiency-first optimizer - completionist collector with moderate status drive - expressive builder with weak progression urgency - growth-focused grinder with low experimentation tolerance - discovery-oriented tinkerer blocked by punishment This is not meant to replace deeper persona work. It is a compact operational summary that helps teams act. ### 7. Recommend design actions Translate the value map into actions such as: - personalize mission framing - surface a different kind of goal - target events/offers more intelligently - reduce pressure toward misaligned systems - give better tools to the dominant value type - redesign progression framing for the current segment - change how rewards are explained, not just what rewards are given - stop over-serving a secondary value while neglecting the dominant one ## Response structure ### Observed Behavior Summary - ... ### Player Value Map | Value | Weight | Confidence | Evidence | |---|---|---|---| | ... | ... | ... | ... | ### Dominant Values - ... ### Secondary Values - ... ### Tensions / Contradictions - ... ### Segment Hypothesis - ... ### Design Implications - ... ### Recommendations 1. ... 2. ... 3. ... ## Fast mode Use this quick pass when speed matters: - what does the player repeatedly choose? - what do they ignore? - what does that imply they value? - what is the strongest mismatch between the player's values and the game's current asks? - what practical segment hypothesis best describes this player? - what should the design emphasize or stop emphasizing for this player? ## Working principle A player rarely says their values directly. They leak them constantly through what they pursue, what they skip, and what they are willing to suffer for.
Audit a game, feature flow, economy path, onboarding journey, progression chain, or live-ops loop for friction quality and friction accumulation. Use when di...
--- name: game-design-friction-journey-audit description: Audit a game, feature flow, economy path, onboarding journey, progression chain, or live-ops loop for friction quality and friction accumulation. Use when diagnosing where players stall, disengage, churn, or feel overloaded; when distinguishing productive challenge from harmful friction; or when evaluating whether constraints, waiting, confusion, resource pressure, or multi-step dependencies are creating strategy, tension, frustration, or deadlock. --- # Game Design Friction Journey Audit Audit a design by mapping where friction appears across a player journey, what kind of friction it is, how it accumulates, and where useful challenge mutates into harmful drag. Use this skill when a feature feels sticky in the wrong way, when progression seems to slow down for reasons players cannot articulate clearly, or when you need to separate meaningful challenge from accidental obstruction. ## Core principle Not all friction is bad. Some friction creates commitment, decision-making, anticipation, and mastery. Other friction creates confusion, paralysis, resentment, or churn. The job is not to remove all resistance. The job is to identify which resistance is doing design work and which is merely getting in the player's way. ## What to produce Generate: 1. **Audit target** - what journey, loop, or feature is being reviewed 2. **Journey breakdown** - the major steps in player progression through the target flow 3. **Friction map** - where friction appears, what kind it is, and what causes it 4. **Accumulation analysis** - where multiple frictions stack into exhaustion or deadlock 5. **Diagnosis** - where the design shifts from meaningful challenge to harmful blockage 6. **Recommendations** - what to preserve, reduce, surface, reorder, or remove ## Process ### 1. Define the journey being audited Clarify: - what system or flow is under review - what kind of player it applies to - what stage of play it belongs to: FTUE, early game, mid-game, elder game, event loop, monetization path, social loop, etc. - what desired player behavior the flow is supposed to support Write: - **Audit target** - **Expected player goal** - **Player context** ### 2. Break the journey into steps Map the journey as a sequence of player-facing steps. For each step, identify: - player action - player decision - requirement or dependency - feedback or reward - what unlocks the next step Keep steps coarse enough to be readable but concrete enough to locate friction. ### 3. Identify friction at each step For each step, ask: - what slows progress? - what blocks progress? - what creates uncertainty? - what consumes time, attention, or resources? - what forces tradeoffs or commitment? Possible friction sources: - resource scarcity - dependency chains - waiting and timers - unclear affordances or goals - UI or information opacity - cognitive overload - skill challenge - social coordination burden - random variance - harsh penalty or recovery cost - monetization pressure ### 4. Classify the friction Classify each friction point as one of these: #### Productive friction Supports: - decision-making - planning - anticipation - mastery - commitment - strategic tradeoff - emotional tension that feels fair and legible #### Harmful friction Produces: - confusion - dead time without meaning - arbitrary blocking - unreadable requirements - overloaded task chains - repeated admin work - punishment without learning - progress paralysis #### Mixed friction Useful in principle, but currently too strong, too opaque, too stacked, or too poorly timed. Do not treat this as binary if it is not. Many systems are good ideas implemented at the wrong intensity. ### 5. Assess intensity and visibility For each friction point, rate: - **Intensity** - low / medium / high - **Visibility** - obvious / partially hidden / opaque - **Fairness feel** - fair / borderline / unfair-feeling A friction can be mild but still dangerous if it is hidden. It can also be intense but acceptable if the player clearly understands it and sees why it exists. ### 6. Analyze friction accumulation Look for stack effects. Ask: - where do several medium frictions compound into a high-friction moment? - where are players forced to satisfy too many constraints at once? - where does the flow ask for too much memory, too much waiting, or too many parallel tasks? - where do repeated harmful frictions appear without enough reward, clarity, or release? Common accumulation patterns: - multiple resources plus timer plus low clarity - complex chain plus weak feedback plus low inventory space - repeated losses plus long recovery plus weak learning signal - social obligation plus schedule pressure plus poor coordination tools ### 7. Find the breakpoints Identify: - where challenge turns into drag - where strategy turns into opacity - where anticipation turns into dead time - where difficulty turns into helplessness - where a healthy loop turns into churn risk These are the key design breakpoints. ### 8. Diagnose the role of friction in the design Answer: - which friction points are core to the fantasy or mastery arc? - which friction points only exist because of weak clarity, weak UX, poor pacing, or over-constrained economy? - what friction is essential and should be protected? - what friction is currently doing accidental damage? ### 9. Recommend design changes For each major friction issue, specify: - **Issue** - **Why it hurts** - **Keep / reduce / remove / surface / reorder / soften** - **Expected effect** Typical interventions: - surface hidden requirements - reduce simultaneous constraints - improve feedback and goal clarity - shorten dead-time without removing commitment - preserve meaningful tradeoffs while removing admin burden - stagger dependencies instead of stacking them all at once ## Response structure ### Audit Target - ... ### Journey Breakdown 1. ... 2. ... 3. ... ### Friction Map | Step | Friction Point | Type | Cause | Intensity | Visibility | Fairness Feel | |---|---|---|---|---|---|---| | ... | ... | ... | ... | ... | ... | ... | ### Accumulation Analysis - ... ### Breakpoints - ... ### Diagnosis - ... ### Recommendations 1. ... 2. ... 3. ... ## Fast mode Use this quick pass when speed matters: - where does the player slow down or stop? - is the friction creating strategy or confusion? - is it fair and legible? - what other frictions are stacking nearby? - what should be preserved, softened, surfaced, or removed? ## Working principle Good friction gives the player something meaningful to push against. Bad friction makes the player wonder why they are pushing at all.
Control QQ Music playback in any browser that exposes a DevTools/CDP endpoint. Supports play/pause/next/prev, search songs/artists/albums, play liked songs,...
---
name: qq-music-web
description: "Control QQ Music playback in any browser that exposes a DevTools/CDP endpoint. Supports play/pause/next/prev, search songs/artists/albums, play liked songs, random play, like/unlike, playlist management (list/create/add-to), and browser-target discovery across platforms."
metadata:
openclaw:
emoji: "🎵"
---
# QQ Music Control
Use this skill to control QQ Music (y.qq.com) through a browser DevTools/CDP endpoint.
## What it supports
- Cross-platform: Windows, macOS, Linux
- Cross-browser: Chrome, Chromium, Edge, Brave, Arc, or any browser exposing a DevTools/CDP endpoint
- Transport: play, pause, toggle, next, previous
- Search & play: songs, artists, albums
- Liked songs: play all, play random, like/unlike current track
- Playlists: list created playlists, create new playlists, add current song to a playlist, play a playlist by ID
- Mode control: list loop, single loop, shuffle, sequential
- Status: current track, artist, time, play state
- Screenshot capture
## Requirements
- **Node.js 18+** (uses built-in `fetch` and `WebSocket`)
- A Chromium-based browser with remote debugging enabled (see setup below)
- A QQ Music account logged in at `y.qq.com` (needed for liked songs, playlists, and like/unlike)
## Setup guide
The skill communicates with the browser via the Chrome DevTools Protocol (CDP). You need to launch your browser with remote debugging enabled so the skill can connect.
### Step 1: Launch browser with remote debugging
Pick one port (e.g. `9222`) and launch your browser with that port. Only one instance can bind to a port.
#### Windows
**Chrome:**
```
"C:\Program Files\Google\Chrome\Application\chrome.exe" --remote-debugging-port=9222
```
**Edge:**
```
"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe" --remote-debugging-port=9222
```
**Brave:**
```
"C:\Program Files\BraveSoftware\Brave-Browser\Application\brave.exe" --remote-debugging-port=9222
```
> On Windows you can also create a desktop shortcut with the flag appended.
#### macOS
**Chrome:**
```bash
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222
```
**Edge:**
```bash
/Applications/Microsoft\ Edge.app/Contents/MacOS/Microsoft\ Edge --remote-debugging-port=9222
```
**Brave:**
```bash
/Applications/Brave\ Browser.app/Contents/MacOS/Brave\ Browser --remote-debugging-port=9222
```
#### Linux
```bash
google-chrome --remote-debugging-port=9222
# or
chromium-browser --remote-debugging-port=9222
# or
brave-browser --remote-debugging-port=9222
```
> **Tip:** Close all existing instances of the browser before launching with the flag, or use a separate profile:
> `--user-data-dir=/tmp/qq-music-profile --remote-debugging-port=9222`
### Step 2: Log in to QQ Music
1. Open `https://y.qq.com/` in the browser you just launched.
2. Log in with your QQ / WeChat account.
3. Optionally open `https://y.qq.com/n/ryqq_v2/player` in another tab for a dedicated player view.
### Step 3: Verify the connection
```bash
node qq-music-ctl.js tabs
```
You should see your browser tabs listed, including the QQ Music ones.
### Step 4 (optional): OpenClaw configuration
If using this skill via OpenClaw and you want the agent to call the script directly:
1. Ensure `plugins.allow` includes `browser` (if using OpenClaw's built-in browser tool as fallback).
2. Add `*.qq.com` and `*.y.qq.com` to `browser.ssrfPolicy.hostnameAllowlist` if SSRF policy is active.
3. Set `browser.ssrfPolicy.dangerouslyAllowPrivateNetwork: true` if the CDP endpoint is on localhost.
## Controller script
All actions go through the bundled script:
```bash
node qq-music-ctl.js <action> [args...]
```
All output is JSON on stdout. Exit code 0 = success, 1 = error.
### Environment variables
| Variable | Default | Description |
|---|---|---|
| `QQ_MUSIC_DEVTOOLS_URL` | _(auto-discover)_ | Explicit DevTools base URL, e.g. `http://127.0.0.1:9222` |
| `QQ_MUSIC_DEVTOOLS_HOST` | `127.0.0.1` | Host to probe for DevTools endpoints |
| `QQ_MUSIC_DEVTOOLS_PORTS` | `19011,9222,9223,9224,9225,9333` | Comma-separated ports to probe |
| `QQ_MUSIC_SCREENSHOT_PATH` | `qq-music-screenshot.png` | Default screenshot output path |
| `QQ_MUSIC_PROBE_TIMEOUT_MS` | `1200` | Per-endpoint probe timeout in ms |
| `QQ_MUSIC_PAGE_WAIT_MS` | `3500` | Wait time after page navigation in ms |
## Action reference
### Playback control
| Action | Description |
|---|---|
| `play` | Resume playback (idempotent) |
| `pause` | Pause playback (idempotent) |
| `toggle` | Toggle play/pause |
| `next` | Next track |
| `prev` | Previous track |
| `status` | Current track, artist, time, duration, play state |
### Search & play
| Action | Description |
|---|---|
| `search <keyword>` | Search for a song and play best match |
| `search-artist <name>` | Search for an artist and open their page |
| `play-artist-all-songs <name>` | Play all songs by an artist |
| `search-album <name>` | Search for an album and play it |
### Liked songs
| Action | Description |
|---|---|
| `play-liked` | Play all liked songs (clicks "播放全部") |
| `play-liked-random` | Randomly play one liked song from the visible page |
| `like` | Like current song (idempotent; returns `already_liked` if already liked) |
| `unlike` | Unlike current song (idempotent; returns `already_unliked` if not liked) |
### Playlists
| Action | Description |
|---|---|
| `list-playlists` | List all created playlists with name, song count, and numeric ID |
| `create-playlist <name>` | Create a new playlist (max 20 characters) |
| `add-to-playlist <name>` | Add the currently playing song to a playlist by name |
| `play-playlist <id>` | Play a playlist by its numeric ID |
### Play mode
| Action | Description |
|---|---|
| `mode` | Show current play mode |
| `mode list` | Set to list loop (列表循环) |
| `mode single` | Set to single loop (单曲循环) |
| `mode random` | Set to shuffle (随机播放) |
| `mode order` | Set to sequential (顺序循环) |
### Utility
| Action | Description |
|---|---|
| `screenshot [path]` | Capture a screenshot of the QQ Music tab |
| `tabs` | List all detectable browser tabs |
| `init` | Open QQ Music if no tab exists |
## How it works
1. **Endpoint discovery**: The script probes localhost ports for a DevTools HTTP endpoint (`/json/version` + `/json/list`). It prefers the endpoint that already has QQ Music tabs open.
2. **Tab selection**: Player-tab (`/player` URL) is preferred for transport controls (play/pause/next/prev/status). A separate browse-tab is used for search, navigation, and playlist operations.
3. **DOM automation**: All interactions use `Runtime.evaluate` over CDP to run JavaScript in the page context. No Puppeteer or Playwright dependency.
4. **No external dependencies**: The script is a single file using only Node.js built-ins (`fs`, `WebSocket`, `fetch`). No `npm install` needed.
## Selection rules
- Prefer the player tab for transport controls.
- Prefer the browse tab for search and playlist discovery.
- If there is no QQ Music tab, `init` opens a blank tab and navigates to `https://y.qq.com/`.
- For song search, the first exact or containing title match wins; otherwise the first visible result is played.
- For liked songs, random play picks from the currently visible page (~10 songs; the web version does not expose all liked songs without scrolling).
- For `add-to-playlist`, if a newly created playlist is not yet visible in the player's menu, the player page is automatically reloaded to refresh the cache and retry.
- `like` and `unlike` are idempotent and report the current state.
- `create-playlist` accepts names up to 20 characters (QQ Music web limit).
## Limitations
- The QQ Music web version shows at most ~10 liked songs per page. `play-liked` uses the "播放全部" button which queues all liked songs in the player, but `play-liked-random` can only pick from the visible ~10.
- System audio volume control is out of scope (OS-level, not browser-controlled).
- Some features (like VIP-only songs) depend on the user's QQ Music subscription.
- The skill does not handle QQ Music login; the user must log in manually first.
## Troubleshooting
- **"No DevTools endpoint found"**: Make sure the browser is running with `--remote-debugging-port=<port>` and no other instance is using that port.
- **"Player not found"**: Play a song first (via `search` or `play-liked`) to make the player tab appear.
- **Timeouts**: Increase `QQ_MUSIC_PAGE_WAIT_MS` for slow connections, or `QQ_MUSIC_PROBE_TIMEOUT_MS` for slow endpoint discovery.
- **"CDP connection closed"**: The page may have navigated or crashed. Retry the command.
## Notes
- The skill does not assume a specific browser brand or OS.
- The skill does not hardcode any personal paths, usernames, or tokens.
- If the browser exposes multiple DevTools endpoints, the controller probes common ports and prefers the one with QQ Music tabs.
FILE:qq-music-ctl.js
#!/usr/bin/env node
/**
* QQ Music browser controller.
*
* Cross-platform and browser-agnostic as long as the browser exposes a
* DevTools / CDP endpoint.
*
* Usage:
* node qq-music-ctl.js <action> [args...]
*
* Environment:
* QQ_MUSIC_DEVTOOLS_URL Explicit DevTools base URL, e.g. http://127.0.0.1:9222
* QQ_MUSIC_DEVTOOLS_HOST Host to probe (default: 127.0.0.1)
* QQ_MUSIC_DEVTOOLS_PORTS Comma-separated probe ports (default: 19011,9222,9223,9224,9225,9333)
* QQ_MUSIC_SCREENSHOT_PATH Output path for screenshots (default: qq-music-screenshot.png)
* QQ_MUSIC_PROBE_TIMEOUT_MS Probe timeout per endpoint (default: 1200)
* QQ_MUSIC_PAGE_WAIT_MS Wait after navigation (default: 3500)
*/
const fs = require('fs');
const DEFAULT_HOST = process.env.QQ_MUSIC_DEVTOOLS_HOST || '127.0.0.1';
const DEFAULT_PORTS = parsePortList(process.env.QQ_MUSIC_DEVTOOLS_PORTS || '19011,9222,9223,9224,9225,9333');
const SCREENSHOT_PATH = process.env.QQ_MUSIC_SCREENSHOT_PATH || 'qq-music-screenshot.png';
const PROBE_TIMEOUT_MS = Number(process.env.QQ_MUSIC_PROBE_TIMEOUT_MS || 1200);
const PAGE_WAIT_MS = Number(process.env.QQ_MUSIC_PAGE_WAIT_MS || 3500);
function parsePortList(value) {
return [...new Set(String(value).split(',').map(s => Number(s.trim())).filter(n => Number.isInteger(n) && n > 0))];
}
function sleep(ms) {
return new Promise(r => setTimeout(r, ms));
}
function timeoutError(label) {
return new Error(`label timed out`);
}
async function fetchJson(url, timeoutMs = PROBE_TIMEOUT_MS) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const res = await fetch(url, { signal: controller.signal });
if (!res.ok) throw new Error(`HTTP res.status`);
return await res.json();
} finally {
clearTimeout(timer);
}
}
function baseOrigin(input) {
const url = new URL(input);
return url.origin;
}
function scoreEndpoint(entry) {
const list = entry.list || [];
const urls = list.map(t => t.url || '');
let score = 0;
if (urls.some(u => u.includes('y.qq.com'))) score += 100;
if (urls.some(u => u.includes('/player'))) score += 30;
if (list.some(t => t.type === 'page')) score += 10;
return score;
}
async function discoverEndpoint() {
const candidates = [];
if (process.env.QQ_MUSIC_DEVTOOLS_URL) candidates.push(baseOrigin(process.env.QQ_MUSIC_DEVTOOLS_URL));
for (const port of DEFAULT_PORTS) candidates.push(`http://DEFAULT_HOST:port`);
const seen = new Set();
const discovered = [];
for (const baseUrl of candidates) {
if (seen.has(baseUrl)) continue;
seen.add(baseUrl);
try {
const [version, list] = await Promise.all([
fetchJson(`baseUrl/json/version`),
fetchJson(`baseUrl/json/list`),
]);
discovered.push({ baseUrl, version, list });
} catch {
// ignore and continue probing
}
}
if (!discovered.length) {
throw new Error(
`No DevTools endpoint found. Set QQ_MUSIC_DEVTOOLS_URL or start a browser with remote debugging. ` +
`Probed ports: DEFAULT_PORTS.join(', ')`
);
}
discovered.sort((a, b) => scoreEndpoint(b) - scoreEndpoint(a));
return discovered[0];
}
function pageTargets(entry) {
return (entry.list || []).filter(t => t.type === 'page');
}
function firstTarget(list, predicate) {
return list.find(predicate) || null;
}
function isQQMusicTarget(target) {
return target && typeof target.url === 'string' && target.url.includes('y.qq.com');
}
function isPlayerTarget(target) {
return isQQMusicTarget(target) && target.url.includes('/player');
}
function isBrowseTarget(target) {
return isQQMusicTarget(target) && !target.url.includes('/player');
}
function prettyUrl(target) {
return target ? target.url : '';
}
function connectCDP(wsUrl) {
return new Promise((resolve, reject) => {
const ws = new WebSocket(wsUrl);
let seq = 0;
let closed = false;
const pending = new Map();
function failAll(err) {
if (closed) return;
closed = true;
for (const { reject: rej, timer } of pending.values()) {
clearTimeout(timer);
rej(err);
}
pending.clear();
}
function send(method, params = {}) {
if (closed) return Promise.reject(new Error('CDP session closed'));
return new Promise((resolveSend, rejectSend) => {
const id = ++seq;
const timer = setTimeout(() => {
pending.delete(id);
rejectSend(timeoutError(method));
}, 10000);
pending.set(id, { resolve: resolveSend, reject: rejectSend, timer });
ws.send(JSON.stringify({ id, method, params }));
});
}
async function evaluate(expression) {
const res = await send('Runtime.evaluate', {
expression,
returnByValue: true,
awaitPromise: true,
});
return res.result ? res.result.value : undefined;
}
ws.onopen = () => resolve({ ws, send, evaluate, close: () => { closed = true; ws.close(); } });
ws.onmessage = evt => {
const msg = JSON.parse(evt.data);
if (!msg.id || !pending.has(msg.id)) return;
const item = pending.get(msg.id);
pending.delete(msg.id);
clearTimeout(item.timer);
if (msg.error) item.reject(new Error(msg.error.message || 'CDP command failed'));
else item.resolve(msg.result);
};
ws.onerror = err => failAll(new Error(err.message || 'CDP connection error'));
ws.onclose = () => failAll(new Error('CDP connection closed'));
});
}
async function browserSession(entry) {
const url = entry.version.webSocketDebuggerUrl;
if (!url) throw new Error('Browser-level WebSocket URL not available. Target.createTarget may not work.');
return connectCDP(url);
}
async function pageSession(target) {
return connectCDP(target.webSocketDebuggerUrl);
}
function output(obj) {
console.log(JSON.stringify(obj, null, 2));
}
async function createTarget(entry, url = 'about:blank') {
const browser = await browserSession(entry);
try {
const result = await browser.send('Target.createTarget', { url });
return result.targetId;
} finally {
browser.close();
}
}
async function openOrReuseBrowseTarget(entry) {
const pages = pageTargets(entry);
const browse = firstTarget(pages, isBrowseTarget);
if (browse) return browse;
const anyQQ = firstTarget(pages, isQQMusicTarget);
if (anyQQ) return anyQQ;
const blank = firstTarget(pages, t => t.url === 'about:blank' || t.url.startsWith('chrome://'));
if (blank) return blank;
const newTargetId = await createTarget(entry, 'about:blank');
const refreshed = await fetchJson(`entry.baseUrl/json/list`);
return firstTarget(refreshed, t => t.id === newTargetId) || firstTarget(refreshed, t => t.url === 'about:blank') || null;
}
function songQueryJS(keyword) {
const q = JSON.stringify(String(keyword || '').trim().toLowerCase());
return `
(function() {
const want = q;
const items = Array.from(document.querySelectorAll('.songlist__item'));
if (!items.length) return JSON.stringify({ ok: false, msg: 'No search results' });
function clean(s) { return String(s || '').trim().toLowerCase().replace(/\s+/g, ''); }
function titleOf(item) {
const el = item.querySelector('.songlist__songname_txt a[title]');
return el ? String(el.title || el.textContent || '').trim() : '';
}
function artistOf(item) {
const el = item.querySelector('.songlist__artist a');
return el ? String(el.title || el.textContent || '').trim() : '';
}
function play(item) {
const btn = item.querySelector('.list_menu__play');
if (btn) { btn.click(); return 'play-btn'; }
const song = item.querySelector('.songlist__songname_txt');
if (song) { song.dispatchEvent(new MouseEvent('dblclick', { bubbles: true, cancelable: true })); return 'dblclick'; }
return 'none';
}
let chosen = items[0];
if (want) {
const exact = items.find(item => clean(titleOf(item)) === want);
const contains = items.find(item => clean(titleOf(item)).includes(want));
chosen = exact || contains || items[0];
}
const name = titleOf(chosen);
const artist = artistOf(chosen);
const method = play(chosen);
return JSON.stringify({ ok: true, song: name, artist, results: items.length, method });
})()
`;
}
function firstVisibleSongJS() {
return `
(function() {
const items = Array.from(document.querySelectorAll('.songlist__item'));
if (!items.length) return JSON.stringify({ ok: false, msg: 'No songs found' });
const idx = Math.floor(Math.random() * items.length);
const item = items[idx];
const nameEl = item.querySelector('.songlist__songname_txt a[title]');
const artistEl = item.querySelector('.songlist__artist a');
const playBtn = item.querySelector('.list_menu__play');
const song = nameEl ? String(nameEl.title || nameEl.textContent || '').trim() : '';
const artist = artistEl ? String(artistEl.title || artistEl.textContent || '').trim() : '';
if (playBtn) playBtn.click(); else item.dispatchEvent(new MouseEvent('dblclick', { bubbles: true, cancelable: true }));
return JSON.stringify({ ok: true, song, artist, index: idx, total: items.length });
})()
`;
}
function playlistPlayJS() {
return `
(function() {
const playAll = document.querySelector('.mod_btn_green');
if (playAll) {
playAll.click();
const items = Array.from(document.querySelectorAll('.songlist__item'));
const first = items[0] ? items[0].querySelector('.songlist__songname_txt a[title]') : null;
return JSON.stringify({ ok: true, action: 'play_all', firstSong: first ? String(first.title || '').trim() : '', total: items.length });
}
const items = Array.from(document.querySelectorAll('.songlist__item'));
if (!items.length) return JSON.stringify({ ok: false, msg: 'Playlist empty or not found' });
const btn = items[0].querySelector('.list_menu__play');
if (btn) btn.click(); else items[0].dispatchEvent(new MouseEvent('dblclick', { bubbles: true, cancelable: true }));
return JSON.stringify({ ok: true, action: 'first_song', total: items.length });
})()
`;
}
async function actionTabs() {
const entry = await discoverEndpoint();
output({
browser: entry.version.Browser || entry.version['Browser'] || '',
baseUrl: entry.baseUrl,
tabs: pageTargets(entry).map(t => ({
id: t.id,
title: t.title,
url: t.url,
isPlayer: isPlayerTarget(t),
isQQMusic: isQQMusicTarget(t),
})),
});
}
async function actionInit() {
const entry = await discoverEndpoint();
const browse = await openOrReuseBrowseTarget(entry);
if (!browse) throw new Error('No browser tab available');
output({ ok: true, baseUrl: entry.baseUrl, targetId: browse.id, url: prettyUrl(browse) });
}
async function withPlayer(fn) {
const entry = await discoverEndpoint();
const target = firstTarget(pageTargets(entry), isPlayerTarget);
if (!target) return output({ error: 'Player not found. Play a song first.' });
const session = await pageSession(target);
try {
return await fn(session, target, entry);
} finally {
session.close();
}
}
async function withBrowse(fn) {
const entry = await discoverEndpoint();
const target = await openOrReuseBrowseTarget(entry);
if (!target) throw new Error('No browser tab available');
const session = await pageSession(target);
try {
return await fn(session, target, entry);
} finally {
session.close();
}
}
async function actionStatus() {
const entry = await discoverEndpoint();
const target = firstTarget(pageTargets(entry), isPlayerTarget);
if (!target) return output({ status: 'no_player', msg: 'QQ Music player not open.' });
const session = await pageSession(target);
try {
const result = await session.evaluate(`
(function() {
const infoEl = document.querySelector('.player_music__info');
const nameEl = infoEl ? infoEl.querySelector('a:first-child') : null;
const artistEl = infoEl ? infoEl.querySelector('a.playlist__author') : null;
const timeEl = document.querySelector('.player_music__time');
const playBtn = document.querySelector('.btn_big_play');
const isPlaying = playBtn ? playBtn.classList.contains('btn_big_play--pause') : null;
const activeSong = document.querySelector('.songlist__item--active .songlist__songname_txt a[title]');
const activeArtist = document.querySelector('.songlist__item--active .songlist__artist a');
let time = '';
let duration = '';
if (timeEl) {
const parts = timeEl.textContent.trim().split('/');
time = (parts[0] || '').trim();
duration = (parts[1] || '').trim();
}
return JSON.stringify({
song: (nameEl ? nameEl.textContent.trim() : '') || (activeSong ? String(activeSong.title || '').trim() : ''),
artist: (artistEl ? artistEl.textContent.trim() : '') || (activeArtist ? String(activeArtist.title || '').trim() : ''),
time,
duration,
isPlaying,
status: isPlaying === true ? 'playing' : isPlaying === false ? 'paused' : 'unknown'
});
})()
`);
output(JSON.parse(result));
} finally {
session.close();
}
}
async function actionPlay() {
await withPlayer(async session => {
const result = await session.evaluate(`
(function() {
const btn = document.querySelector('.btn_big_play');
if (!btn) return JSON.stringify({ ok: false, msg: 'Play button not found' });
const wasPlaying = btn.classList.contains('btn_big_play--pause');
if (!wasPlaying) btn.click();
return JSON.stringify({ ok: true, action: wasPlaying ? 'already_playing' : 'resumed' });
})()
`);
output(JSON.parse(result));
});
}
async function actionPause() {
await withPlayer(async session => {
const result = await session.evaluate(`
(function() {
const btn = document.querySelector('.btn_big_play');
if (!btn) return JSON.stringify({ ok: false, msg: 'Play button not found' });
const wasPlaying = btn.classList.contains('btn_big_play--pause');
if (wasPlaying) btn.click();
return JSON.stringify({ ok: true, action: wasPlaying ? 'paused' : 'already_paused' });
})()
`);
output(JSON.parse(result));
});
}
async function actionToggle() {
await withPlayer(async session => {
const result = await session.evaluate(`
(function() {
const btn = document.querySelector('.btn_big_play');
if (!btn) return JSON.stringify({ ok: false, msg: 'Play button not found' });
const wasPlaying = btn.classList.contains('btn_big_play--pause');
btn.click();
return JSON.stringify({ ok: true, action: wasPlaying ? 'pause' : 'play' });
})()
`);
output(JSON.parse(result));
});
}
async function actionNext() {
await withPlayer(async session => {
const result = await session.evaluate(`
(function() {
const btn = document.querySelector('.btn_big_next');
if (btn) { btn.click(); return JSON.stringify({ ok: true, action: 'next' }); }
return JSON.stringify({ ok: false, msg: 'Next button not found' });
})()
`);
output(JSON.parse(result));
});
}
async function actionPrev() {
await withPlayer(async session => {
const result = await session.evaluate(`
(function() {
const btn = document.querySelector('.btn_big_prev');
if (btn) { btn.click(); return JSON.stringify({ ok: true, action: 'prev' }); }
return JSON.stringify({ ok: false, msg: 'Prev button not found' });
})()
`);
output(JSON.parse(result));
});
}
function normalizeMusicText(value) {
return String(value || '')
.trim()
.toLowerCase()
.replace(/\s+/g, '')
.replace(/[·•]/g, '')
.replace(/[()()\[\]【】{}]/g, '');
}
async function waitForEvalResult(session, buildEvalJs, { timeoutMs = 12000, intervalMs = 350, label = 'condition' } = {}) {
const deadline = Date.now() + timeoutMs;
let last = null;
while (Date.now() < deadline) {
try {
const raw = await session.evaluate(buildEvalJs());
last = JSON.parse(raw);
} catch (err) {
last = { ok: false, stage: 'evaluate_error', error: err.message || String(err) };
}
if (last && last.ok) return last;
await sleep(intervalMs);
}
const error = new Error(`label timed out`);
error.last = last;
throw error;
}
function buildArtistSearchEval(keyword) {
const want = JSON.stringify(normalizeMusicText(keyword));
return `
(function() {
const want = want;
const norm = s => String(s || '')
.trim()
.toLowerCase()
.replace(/\\s+/g, '')
.replace(/[·•]/g, '')
.replace(/[()()\\[\\]【】{}]/g, '');
const selectors = [
'.search_result__singer a',
'.singer_list__item a',
'.mod_singer_list a',
'a[href*="/singer/"]',
'a[href*="/ryqq/singer/"]'
];
const seen = new Set();
const candidates = Array.from(document.querySelectorAll(selectors.join(','))).filter(el => {
const href = String(el.href || el.getAttribute('href') || '').trim();
const text = norm(el.title || el.textContent || el.getAttribute('aria-label') || '');
if (!href && !text) return false;
const key = href + '|' + text;
if (seen.has(key)) return false;
seen.add(key);
return true;
});
const match = candidates.find(el => {
const text = norm(el.title || el.textContent || el.getAttribute('aria-label') || '');
return want && text && (text === want || text.includes(want) || want.includes(text));
});
if (!match) {
return JSON.stringify({
ok: false,
stage: 'searching',
count: candidates.length
});
}
const rawHref = match.href || match.getAttribute('href') || '';
let href = '';
try {
href = rawHref ? new URL(rawHref, location.href).href : '';
} catch {
href = rawHref;
}
return JSON.stringify({
ok: true,
name: String(match.title || match.textContent || match.getAttribute('aria-label') || '').trim(),
href,
count: candidates.length
});
})()
`;
}
function buildPlayAllEval() {
return `
(function() {
const norm = s => String(s || '').trim();
const selectors = [
'.mod_btn_green',
'.btn_green',
'.songlist__play',
'[title*="播放全部"]',
'[title*="全部播放"]',
'[aria-label*="播放全部"]',
'[aria-label*="全部播放"]'
];
const candidates = Array.from(document.querySelectorAll(selectors.join(',')));
const button = candidates.find(el => {
const text = norm(el.title || el.textContent || el.getAttribute('aria-label') || '');
return text.includes('播放全部') || text.includes('全部播放') || text.includes('播放歌手热门歌曲') || (text.includes('播放') && text.includes('全部'));
});
if (!button) {
return JSON.stringify({ ok: false, stage: 'play_all_not_found', count: candidates.length });
}
button.scrollIntoView({ block: 'center' });
button.click();
return JSON.stringify({
ok: true,
action: 'play_all_clicked',
label: norm(button.title || button.textContent || button.getAttribute('aria-label') || '')
});
})()
`;
}
async function openArtistPage(session, keyword) {
const query = String(keyword || '').trim();
if (!query) throw new Error('Artist keyword is required');
const searchUrl = `https://y.qq.com/n/ryqq/search?w=encodeURIComponent(query)&t=singer`;
await session.send('Page.navigate', { url: searchUrl });
await sleep(800);
const result = await waitForEvalResult(
session,
() => buildArtistSearchEval(query),
{ timeoutMs: 15000, intervalMs: 400, label: `search artist query` }
);
if (!result.href) {
throw new Error(`Artist link not found for query`);
}
await session.send('Page.navigate', { url: result.href });
await sleep(1000);
return result;
}
async function actionSearch(keyword, type = 'song') {
await withBrowse(async session => {
const typeMap = { song: 'song', album: 'album' };
const t = typeMap[type] || 'song';
const url = `https://y.qq.com/n/ryqq/search?w=encodeURIComponent(String(keyword || '').trim())&t=t`;
await session.send('Page.navigate', { url });
await sleep(PAGE_WAIT_MS);
if (type === 'album') {
const playAll = await session.evaluate(buildPlayAllEval());
const parsedPlayAll = JSON.parse(playAll);
if (parsedPlayAll.ok) {
output({ ok: true, scope: 'album', ...parsedPlayAll });
return;
}
const result = await session.evaluate(`
(function() {
const items = Array.from(document.querySelectorAll('.songlist__item'));
if (!items.length) return JSON.stringify({ ok: false, msg: 'No results' });
const item = items[0];
const nameEl = item.querySelector('.songlist__songname_txt a[title]');
const artistEl = item.querySelector('.songlist__artist a');
const playBtn = item.querySelector('.list_menu__play');
if (playBtn) playBtn.click(); else item.dispatchEvent(new MouseEvent('dblclick', { bubbles: true, cancelable: true }));
return JSON.stringify({ ok: true, song: nameEl ? String(nameEl.title || '').trim() : '', artist: artistEl ? String(artistEl.title || '').trim() : '', fallback: 'first_song' });
})()
`);
output(JSON.parse(result));
return;
}
const result = await session.evaluate(songQueryJS(keyword));
output(JSON.parse(result));
});
}
async function actionSearchArtist(keyword) {
await withBrowse(async session => {
const artist = await openArtistPage(session, keyword);
output({
ok: true,
action: 'opened_artist_page',
artist: artist.name,
href: artist.href,
count: artist.count,
});
});
}
async function actionPlayArtistAllSongs(keyword) {
await withBrowse(async session => {
const artist = await openArtistPage(session, keyword);
const result = await waitForEvalResult(
session,
buildPlayAllEval,
{ timeoutMs: 15000, intervalMs: 450, label: `play all songs for artist.name || String(keyword || '').trim()` }
);
output({
ok: true,
action: 'play_artist_all_songs',
artist: artist.name,
href: artist.href,
...result,
});
});
}
async function actionPlayLiked(random = false) {
await withBrowse(async session => {
await session.send('Page.navigate', { url: 'https://y.qq.com/n/ryqq_v2/profile/like/song' });
await sleep(PAGE_WAIT_MS);
if (random) {
const result = await session.evaluate(firstVisibleSongJS());
output(JSON.parse(result));
} else {
// Click "播放全部" to queue all liked songs
const playAllResult = await session.evaluate(buildPlayAllEval());
const parsed = JSON.parse(playAllResult);
if (parsed.ok) {
output({ ok: true, action: 'play_all_liked', ...parsed });
} else {
// Fallback: play first song
const result = await session.evaluate(`
(function() {
const items = Array.from(document.querySelectorAll('.songlist__item'));
if (!items.length) return JSON.stringify({ ok: false, msg: 'No liked songs found' });
const item = items[0];
const nameEl = item.querySelector('.songlist__songname_txt a[title]');
const artistEl = item.querySelector('.songlist__artist a');
const playBtn = item.querySelector('.list_menu__play');
if (playBtn) playBtn.click(); else item.dispatchEvent(new MouseEvent('dblclick', { bubbles: true, cancelable: true }));
return JSON.stringify({ ok: true, song: nameEl ? String(nameEl.title || '').trim() : '', artist: artistEl ? String(artistEl.title || '').trim() : '', index: 0, total: items.length });
})()
`);
output(JSON.parse(result));
}
}
});
}
async function actionPlayPlaylist(playlistId) {
await withBrowse(async session => {
await session.send('Page.navigate', { url: `https://y.qq.com/n/ryqq/playlist/encodeURIComponent(String(playlistId || '').trim())` });
await sleep(PAGE_WAIT_MS);
const result = await session.evaluate(playlistPlayJS());
output(JSON.parse(result));
});
}
async function actionLike() {
await withPlayer(async session => {
const result = await session.evaluate(`
(function() {
const btn = document.querySelector('.btn_big_like');
if (!btn) return JSON.stringify({ ok: false, msg: 'Like button not found' });
const wasLiked = btn.classList.contains('btn_big_like--like');
if (wasLiked) return JSON.stringify({ ok: true, action: 'already_liked', liked: true });
btn.click();
return JSON.stringify({ ok: true, action: 'liked', liked: true });
})()
`);
output(JSON.parse(result));
});
}
async function actionUnlike() {
await withPlayer(async session => {
const result = await session.evaluate(`
(function() {
const btn = document.querySelector('.btn_big_like');
if (!btn) return JSON.stringify({ ok: false, msg: 'Like button not found' });
const wasLiked = btn.classList.contains('btn_big_like--like');
if (!wasLiked) return JSON.stringify({ ok: true, action: 'already_unliked', liked: false });
btn.click();
return JSON.stringify({ ok: true, action: 'unliked', liked: false });
})()
`);
output(JSON.parse(result));
});
}
async function actionListPlaylists() {
await withBrowse(async session => {
await session.send('Page.navigate', { url: 'https://y.qq.com/n/ryqq_v2/profile/create' });
await sleep(PAGE_WAIT_MS);
const result = await waitForEvalResult(
session,
() => `
(function() {
const items = Array.from(document.querySelectorAll('.playlist__item'));
if (!items.length) return JSON.stringify({ ok: false, msg: 'No playlists found' });
const playlists = items.map(item => {
const titleEl = item.querySelector('.playlist__title');
const numberEl = item.querySelector('.playlist__number');
const linkEl = item.querySelector('a[href*="playlist"]');
const href = linkEl ? String(linkEl.href || '') : '';
const parts = href.split('/');
const id = parts[parts.length - 1] || '';
return {
name: titleEl ? titleEl.textContent.trim() : '',
count: numberEl ? numberEl.textContent.trim() : '',
id: id,
};
});
return JSON.stringify({ ok: true, playlists });
})()
`,
{ timeoutMs: 15000, intervalMs: 500, label: 'list playlists' }
);
output(result);
});
}
async function actionCreatePlaylist(name) {
const playlistName = String(name || '').trim();
if (!playlistName) throw new Error('Playlist name is required');
await withBrowse(async session => {
await session.send('Page.navigate', { url: 'https://y.qq.com/n/ryqq_v2/profile/create' });
await sleep(PAGE_WAIT_MS);
// Click "新建歌单" button
await session.evaluate(`
(function() {
const btn = document.querySelector('.js_create_new');
if (btn) btn.click();
})()
`);
await sleep(1000);
// Fill in name and confirm
const nameEscaped = JSON.stringify(playlistName);
const result = await session.evaluate(`
(function() {
const input = document.querySelector('#new_playlist');
if (!input) return JSON.stringify({ ok: false, msg: 'Create dialog not found' });
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
nativeInputValueSetter.call(input, nameEscaped);
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
const confirmBtn = document.querySelector('.popup__ft .mod_btn_green');
if (!confirmBtn) return JSON.stringify({ ok: false, msg: 'Confirm button not found' });
confirmBtn.click();
return JSON.stringify({ ok: true, action: 'created', name: nameEscaped });
})()
`);
output(JSON.parse(result));
});
}
async function addToPlaylistAttempt(playerTarget, want) {
const session = await pageSession(playerTarget);
try {
// Click add button on the currently playing song
const raw = await session.evaluate(`
(function() {
const playing = document.querySelector('.songlist__item--playing');
if (!playing) return JSON.stringify({ ok: false, msg: 'No song playing' });
const addBtn = playing.querySelector('.list_menu__add');
if (addBtn) addBtn.click();
return JSON.stringify({ ok: true, clicked: !!addBtn });
})()
`);
const clickResult = JSON.parse(raw);
if (!clickResult.ok) return clickResult;
} finally {
session.close();
}
await sleep(1000);
const session2 = await pageSession(playerTarget);
try {
const raw2 = await session2.evaluate(`
(function() {
const want = JSON.stringify(want);
const menu = document.querySelector('.mod_operate_menu');
if (!menu) return JSON.stringify({ ok: false, msg: 'Add-to-playlist menu not found' });
const items = Array.from(menu.querySelectorAll('.operate_menu__item .operate_menu__link'));
const match = items.find(a => a.textContent.trim().toLowerCase() === want);
if (!match) {
const available = items.map(a => a.textContent.trim());
return JSON.stringify({ ok: false, msg: 'Playlist not found', available });
}
match.click();
return JSON.stringify({ ok: true, action: 'added', playlist: match.textContent.trim() });
})()
`);
return JSON.parse(raw2);
} finally {
session2.close();
}
}
async function actionAddToPlaylist(playlistName) {
const want = String(playlistName || '').trim().toLowerCase();
if (!want) throw new Error('Playlist name is required');
const entry = await discoverEndpoint();
const playerTarget = firstTarget(pageTargets(entry), isPlayerTarget);
if (!playerTarget) return output({ error: 'Player not found. Play a song first.' });
let result = await addToPlaylistAttempt(playerTarget, want);
// If playlist not found, reload player to refresh playlist cache and retry
if (!result.ok && result.msg === 'Playlist not found') {
const reloadSession = await pageSession(playerTarget);
try {
await reloadSession.evaluate('location.reload()');
} finally {
reloadSession.close();
}
await sleep(PAGE_WAIT_MS);
result = await addToPlaylistAttempt(playerTarget, want);
}
output(result);
}
async function actionScreenshot(pathArg) {
const entry = await discoverEndpoint();
const target = firstTarget(pageTargets(entry), isPlayerTarget) || firstTarget(pageTargets(entry), isBrowseTarget);
if (!target) return output({ error: 'No QQ Music tab found.' });
const session = await pageSession(target);
try {
await sleep(1000);
const result = await session.send('Page.captureScreenshot', { format: 'png' });
const outPath = pathArg || SCREENSHOT_PATH;
const buf = Buffer.from(result.data, 'base64');
fs.writeFileSync(outPath, buf);
output({ ok: true, path: outPath, bytes: buf.length });
} finally {
session.close();
}
}
const PLAY_MODES = {
'list': { class: 'btn_big_style_list', label: '列表循环' },
'single': { class: 'btn_big_style_single', label: '单曲循环' },
'random': { class: 'btn_big_style_random', label: '随机播放' },
'order': { class: 'btn_big_style_order', label: '顺序循环' },
};
const MODE_CYCLE = ['list', 'single', 'random', 'order'];
function detectCurrentMode(className) {
for (const [key, val] of Object.entries(PLAY_MODES)) {
if (className.includes(val.class)) return key;
}
return null;
}
async function actionMode(targetMode) {
await withPlayer(async session => {
if (targetMode && !PLAY_MODES[targetMode]) {
return output({ ok: false, msg: `Unknown mode: targetMode. Valid: Object.keys(PLAY_MODES).join(', ')` });
}
const current = await session.evaluate(`
(() => {
const el = document.querySelector('[class*=btn_big_style]');
if (!el) return JSON.stringify({ error: 'Mode button not found' });
return JSON.stringify({ className: el.className, title: el.title });
})()
`);
const info = JSON.parse(current);
if (info.error) return output({ ok: false, msg: info.error });
const currentMode = detectCurrentMode(info.className);
if (!targetMode) {
return output({ ok: true, mode: currentMode, label: PLAY_MODES[currentMode]?.label || info.title });
}
if (currentMode === targetMode) {
return output({ ok: true, mode: currentMode, label: PLAY_MODES[currentMode].label, action: 'already_set' });
}
const maxClicks = MODE_CYCLE.length;
for (let i = 0; i < maxClicks; i++) {
const result = await session.evaluate(`
(() => {
const el = document.querySelector('[class*=btn_big_style]');
if (!el) return JSON.stringify({ error: 'Mode button not found' });
el.click();
return new Promise(r => setTimeout(() => {
r(JSON.stringify({ className: el.className, title: el.title }));
}, 500));
})()
`);
const after = JSON.parse(result);
if (after.error) return output({ ok: false, msg: after.error });
const newMode = detectCurrentMode(after.className);
if (newMode === targetMode) {
return output({ ok: true, mode: newMode, label: PLAY_MODES[newMode].label, action: 'switched', clicks: i + 1 });
}
}
return output({ ok: false, msg: `Failed to switch to targetMode after maxClicks clicks` });
});
}
function printHelp() {
output({
usage: 'node qq-music-ctl.js <action> [args...]',
actions: ['play','pause','toggle','next','prev','status','mode [list|single|random|order]','search <keyword>','search-artist <artist>','play-artist-all-songs <artist>','search-album <album>','play-liked','play-liked-random','play-playlist <id>','like','unlike','list-playlists','create-playlist <name>','add-to-playlist <name>','screenshot [path]','tabs','init'],
});
}
async function main() {
const action = process.argv[2];
const args = process.argv.slice(3);
if (!action || action === '--help' || action === '-h') {
return printHelp();
}
switch (action) {
case 'play': return actionPlay();
case 'pause': return actionPause();
case 'toggle': return actionToggle();
case 'next': return actionNext();
case 'prev': return actionPrev();
case 'status': return actionStatus();
case 'search': return actionSearch(args.join(' '), 'song');
case 'search-artist': return actionSearchArtist(args.join(' '));
case 'play-artist-all-songs': return actionPlayArtistAllSongs(args.join(' '));
case 'search-album': return actionSearch(args.join(' '), 'album');
case 'play-liked': return actionPlayLiked(false);
case 'play-liked-random': return actionPlayLiked(true);
case 'play-playlist': return actionPlayPlaylist(args[0]);
case 'mode': return actionMode(args[0] || '');
case 'like': return actionLike();
case 'unlike': return actionUnlike();
case 'list-playlists': return actionListPlaylists();
case 'create-playlist': return actionCreatePlaylist(args.join(' '));
case 'add-to-playlist': return actionAddToPlaylist(args.join(' '));
case 'screenshot': return actionScreenshot(args[0]);
case 'tabs': return actionTabs();
case 'init': return actionInit();
default:
return printHelp();
}
}
main().catch(err => {
output({ error: err.message || String(err) });
process.exit(1);
});
Ajinomoto is a Japanese biotech firm that commercialized umami and leads global production of MSG and amino acid products for food and pharma.
---
summary: Ajinomoto is a Japanese multinational food and biotechnology company that discovered and commercialized umami — the fifth taste — and remains the world's largest producer of monosodium glutamate (MSG) and amino acid products.
read_when:
- Studying the global food science and flavor industry
- Analyzing Ajinomoto expansion from MSG to biotechnology and pharma
- Researching umami taste science and its impact on global cuisine
- Understanding Japanese corporate innovation in food technology
---
# Ajinomoto
## Overview
Ajinomoto is a Japanese multinational food and biotechnology company that discovered and commercialized umami — the fifth taste — and remains the world's largest producer of monosodium glutamate (MSG) and amino acid products.
## Historical Timeline
- 1909: Kikunae Ikeda discovers umami taste and patents MSG production
- 1925: Ajinomoto Co formally established in Tokyo
- 1956: Discovers industrial fermentation process for amino acid production
- 1980s: Expands into pharmaceuticals and biotechnology
- 2000: Launches 'Eat Well, Live Well' brand transformation
- 2024: Announces major investment in cultivated meat and alternative protein
## Business Model
Three segments: Seasonings and Foods (45%), AminoScience (35% — pharma, animal nutrition, sweeteners), and Frozen Foods (20%). Revenue from B2C food products (Ajinomoto brand MSG, Cook Do sauce mixes) and B2B amino acid ingredients for pharmaceutical and animal feed industries.
## Moat Analysis
Proprietary fermentation technology for amino acid production — over 100 years of process optimization. Umami discovery gives scientific credibility and brand authority in flavor science. Vertical integration from raw materials to finished food products.
## Key Data
- revenue: ~¥1.3 trillion (~$9B) (2023)
- msg_production: ~30% of global supply
- employees: ~37,000
- countries: ~80+
- r_and_d: ~¥40B/year
## Interesting Facts
- Professor Kikunae Ikeda discovered umami by tasting dashi broth and identifying glutamate as the source — he then crystallized it from kombu seaweed and patented the extraction process.
- Despite global MSG stigma in Western markets, Ajinomoto's MSG production has never stopped growing — it is now used in 90%+ of processed foods worldwide.
Generate QR codes from URLs or text. Export as PNG with customizable size. No API key required.
--- slug: qrcode-tool name: QR Code Generator description: "Generate QR codes from URLs or text. Export as PNG with customizable size. No API key required." keywords: qrcode, qr, barcode, generator, url, text version: "1.0.0" author: Qiance language: en --- # QR Code Generator Generate QR codes from any text or URL. Supports customization and exports as PNG format. ## Features - Generate QR codes from any text/URL - Custom size (default 300px) - Custom margin - Export as PNG format - No API key required ## Usage ```bash # Generate QR code for URL python3 scripts/qrcode_generator.py "https://example.com" # Generate QR code for text python3 scripts/qrcode_generator.py "Hello World" # Custom size python3 scripts/qrcode_generator.py "https://example.com" --size 500 ``` ## Examples ``` Generate QR code for: https://github.com Generate QR code for: Contact me at [email protected] Generate QR code for: WIFI:T:WPA;S:MyNetwork;P:password;; ``` ## Technical Details - Uses qrserver.com public API - SSL certificate verification enabled (certifi) - No sensitive data transmission ## Dependencies - Python 3.7+ - certifi (SSL certificates) ## Privacy Note Input text is sent to api.qrserver.com (third-party service). Not recommended for sensitive information. --- ## 中文说明 输入URL或文本,生成PNG二维码。 - 自定义尺寸(默认300px) - 无需API Key - 使用qrserver.com公开API FILE:README.md # QR Code Generator Generate QR codes from any text or URL with customizable options. ## Installation No installation required. Uses Python standard library + certifi for SSL. ```bash pip install certifi # Optional but recommended for SSL verification ``` ## Usage ### Basic Usage ```bash # Generate QR code for URL python3 scripts/qrcode_generator.py "https://example.com" # Generate QR code for text python3 scripts/qrcode_generator.py "Hello World" ``` ### Advanced Options ```bash # Custom size python3 scripts/qrcode_generator.py "https://example.com" --size 500 # Custom margin python3 scripts/qrcode_generator.py "Hello" --margin 2 # Different format python3 scripts/qrcode_generator.py "Test" --format gif # JSON output python3 scripts/qrcode_generator.py "https://example.com" --json ``` ## Examples | Input | Use Case | |-------|----------| | `https://github.com` | Website URL | | `mailto:[email protected]` | Email link | | `tel:+1234567890` | Phone number | | `WIFI:T:WPA;S:MyNetwork;P:password;;` | WiFi credentials | | `Hello World` | Plain text | ## API Uses the free [qrserver.com API](https://api.qrserver.com). No API key required. ## Privacy ⚠️ Input text is sent to a third-party API (api.qrserver.com). Do not use for sensitive information like passwords or private keys. ## License MIT License FILE:scripts/qrcode_generator.py #!/usr/bin/env python3 """QR Code Generator - Generate QR codes from text/URL""" import sys import os import base64 import ssl import urllib.request import urllib.parse import argparse try: import certifi CERTIFI_AVAILABLE = True except ImportError: CERTIFI_AVAILABLE = False def generate_qrcode(text, size=300, margin=4, format='png'): """Generate QR code using qrserver.com API Args: text: Text or URL to encode size: QR code size in pixels (default 300) margin: Margin around QR code (default 4) format: Output format (default png) Returns: dict with success status and data/base64 or error message """ encoded_text = urllib.parse.quote(text) url = f"https://api.qrserver.com/v1/create-qr-code/?size={size}x{size}&margin={margin}&format={format}&data={encoded_text}" headers = { 'User-Agent': 'QRCode-Tool/1.0 (https://github.com/qiance)' } try: # SSL with certifi if available, fallback to default if CERTIFI_AVAILABLE: ctx = ssl.create_default_context() ctx.load_verify_locations(certifi.where()) else: ctx = ssl.create_default_context() req = urllib.request.Request(url, headers=headers) response = urllib.request.urlopen(req, timeout=15, context=ctx) data = response.read() return { "success": True, "data": f"data:image/{format};base64,{base64.b64encode(data).decode()}", "url": url, "size": size, "text_length": len(text) } except Exception as e: return {"success": False, "error": str(e)} def main(): parser = argparse.ArgumentParser( description='Generate QR codes from text or URLs', formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: python3 qrcode_generator.py "https://example.com" python3 qrcode_generator.py "Hello World" --size 500 python3 qrcode_generator.py "WIFI:T:WPA;S:MyNetwork;P:password;;" --margin 2 """ ) parser.add_argument('text', help='Text or URL to encode') parser.add_argument('--size', type=int, default=300, help='QR code size in pixels (default: 300)') parser.add_argument('--margin', type=int, default=4, help='Margin around QR code (default: 4)') parser.add_argument('--format', choices=['png', 'gif', 'jpeg', 'jpg'], default='png', help='Output format (default: png)') parser.add_argument('--json', action='store_true', help='Output as JSON') args = parser.parse_args() result = generate_qrcode(args.text, args.size, args.margin, args.format) if args.json: import json print(json.dumps(result, indent=2)) else: if result["success"]: print(f"✅ QR Code generated successfully!") print(f" Size: {result['size']}x{result['size']} pixels") print(f" Text length: {result['text_length']} characters") print(f" Base64 length: {len(result['data'])} characters") else: print(f"❌ Failed to generate QR code: {result['error']}") if __name__ == '__main__': main()
支持数学表达式计算和单位换算,包含四则运算、科学函数及常用常量,纯本地安全计算无外部依赖。
# cn-math-calculator
数学表达式计算器。支持基本运算、科学计算、单位换算。
## 功能
- 四则运算 + - * / ^(幂) %(取模)
- 科学函数:sin, cos, tan, log, sqrt, abs
- 常量:pi, e
- 单位换算:长度、重量、温度、面积
- 表达式安全求值(不使用eval)
- 纯本地处理,无需API
## 安装要求
- Python 3.6+
- 无外部依赖
## 使用方法
```
千策,计算 2^10 + 100
千策,计算 sqrt(144)
千策,换算 100公里等于多少英里
```
## 参数
- `expression`: 数学表达式
- `convert`: 单位换算格式 (数值 原单位 -> 目标单位)
## 示例
输入:
```
千策,计算 (100 + 50) * 2 - 30
```
输出:
```
结果: 270
```
## 分类
工具
## 关键词
计算器, 数学, calculator, math, 单位换算
FILE:scripts/math_calculator.py
#!/usr/bin/env python3
"""
数学表达式计算器
安全求值,支持科学函数和单位换算
"""
import argparse
import sys
import json
import math
import re
from typing import Dict, Any
# 安全的数学函数映射
SAFE_FUNCTIONS = {
'sin': math.sin,
'cos': math.cos,
'tan': math.tan,
'asin': math.asin,
'acos': math.acos,
'atan': math.atan,
'sinh': math.sinh,
'cosh': math.cosh,
'tanh': math.tanh,
'log': math.log10,
'ln': math.log,
'log2': math.log2,
'sqrt': math.sqrt,
'abs': abs,
'floor': math.floor,
'ceil': math.ceil,
'round': round,
'exp': math.exp,
'pow': pow,
}
SAFE_CONSTANTS = {
'pi': math.pi,
'e': math.e,
}
# 单位换算表
UNIT_CONVERSIONS = {
# 长度 (到米的换算因子)
'length': {
'km': 1000, '公里': 1000, '千米': 1000,
'm': 1, '米': 1,
'cm': 0.01, '厘米': 0.01,
'mm': 0.001, '毫米': 0.001,
'mile': 1609.344, '英里': 1609.344,
'yard': 0.9144, '码': 0.9144,
'ft': 0.3048, '英尺': 0.3048,
'inch': 0.0254, '英寸': 0.0254,
'里': 500, '丈': 3.333, '尺': 0.333, '寸': 0.0333,
},
# 重量 (到千克的换算因子)
'weight': {
't': 1000, '吨': 1000,
'kg': 1, '千克': 1, '公斤': 1,
'g': 0.001, '克': 0.001,
'mg': 0.000001, '毫克': 0.000001,
'lb': 0.453592, '磅': 0.453592,
'oz': 0.0283495, '盎司': 0.0283495,
'斤': 0.5, '两': 0.05, '钱': 0.005,
},
# 温度 (特殊处理)
'temperature': {
'c': 'c', '摄氏度': 'c', '摄氏': 'c',
'f': 'f', '华氏度': 'f', '华氏': 'f',
'k': 'k', '开尔文': 'k',
},
# 面积 (到平方米的换算因子)
'area': {
'km2': 1e6, '平方公里': 1e6,
'm2': 1, '平方米': 1, '平米': 1,
'cm2': 0.0001, '平方厘米': 0.0001,
'ha': 10000, '公顷': 10000,
'acre': 4046.86, '英亩': 4046.86,
'亩': 666.67,
},
}
def safe_eval(expression: str) -> float:
"""
安全地计算数学表达式
"""
# 预处理:替换常量
expr = expression.lower()
for const, value in SAFE_CONSTANTS.items():
expr = expr.replace(const, str(value))
# 替换函数调用为前缀形式
for func in SAFE_FUNCTIONS:
expr = re.sub(rf'\b{func}\s*\(', f'__{func}__(', expr, flags=re.IGNORECASE)
# 安全检查:只允许数字、运算符、括号和函数调用
allowed = r'^[\d\s\+\-\*\/\%\^\(\)\.\_a-z]+$'
if not re.match(allowed, expr):
raise ValueError(f"表达式包含非法字符: {expression}")
# 替换运算符
expr = expr.replace('^', '**')
# 构建安全的命名空间
namespace = {f'__{f}__': func for f, func in SAFE_FUNCTIONS.items()}
try:
result = eval(expr, {"__builtins__": {}}, namespace)
return float(result)
except Exception as e:
raise ValueError(f"计算错误: {e}")
def convert_temperature(value: float, from_unit: str, to_unit: str) -> float:
"""
温度换算
"""
# 转换为摄氏度
if from_unit == 'c':
celsius = value
elif from_unit == 'f':
celsius = (value - 32) * 5 / 9
elif from_unit == 'k':
celsius = value - 273.15
else:
raise ValueError(f"不支持的温度单位: {from_unit}")
# 从摄氏度转换到目标单位
if to_unit == 'c':
return celsius
elif to_unit == 'f':
return celsius * 9 / 5 + 32
elif to_unit == 'k':
return celsius + 273.15
else:
raise ValueError(f"不支持的温度单位: {to_unit}")
def convert_unit(value: float, from_unit: str, to_unit: str) -> float:
"""
单位换算
"""
from_unit = from_unit.lower().strip()
to_unit = to_unit.lower().strip()
if from_unit == to_unit:
return value
# 查找单位所属类别
for category, units in UNIT_CONVERSIONS.items():
if from_unit in units and to_unit in units:
if category == 'temperature':
return convert_temperature(value, units[from_unit], units[to_unit])
else:
factor_from = units[from_unit]
factor_to = units[to_unit]
return value * factor_from / factor_to
raise ValueError(f"不支持的单位换算: {from_unit} -> {to_unit}")
def parse_convert_request(text: str) -> tuple:
"""
解析单位换算请求
格式: "100公里等于多少英里" 或 "100 km to miles"
"""
# 中文格式
cn_pattern = r'([\d\.]+)\s*([^\s等于]+?)\s*等于?\s*(?:多少)?\s*([^\s]+)'
match = re.search(cn_pattern, text)
if match:
value = float(match.group(1))
from_unit = match.group(2)
to_unit = match.group(3)
return value, from_unit, to_unit
# 英文格式 "100 km to miles"
en_pattern = r'([\d\.]+)\s*(\w+)\s+to\s+(\w+)'
match = re.search(en_pattern, text, re.IGNORECASE)
if match:
value = float(match.group(1))
from_unit = match.group(2)
to_unit = match.group(3)
return value, from_unit, to_unit
return None, None, None
def main():
parser = argparse.ArgumentParser(description="数学表达式计算器")
parser.add_argument("expression", nargs="?", help="数学表达式")
parser.add_argument("-c", "--convert", help="单位换算")
parser.add_argument("-j", "--json", action="store_true", help="JSON输出")
args = parser.parse_args()
result = None
error = None
try:
if args.convert:
# 单位换算模式
value, from_unit, to_unit = parse_convert_request(args.convert)
if value is None:
value, from_unit, to_unit = parse_convert_request(args.expression)
if value is not None:
result = convert_unit(value, from_unit, to_unit)
else:
error = "无法解析单位换算请求"
elif args.expression:
# 表达式计算模式
result = safe_eval(args.expression)
else:
error = "请提供数学表达式或换算请求"
except Exception as e:
error = str(e)
if args.json:
output = {
"success": result is not None,
"result": result,
"error": error
}
print(json.dumps(output, ensure_ascii=False, indent=2))
else:
if error:
print(f"错误: {error}", file=sys.stderr)
sys.exit(1)
else:
print(f"结果: {result}")
if __name__ == "__main__":
main()


Luxury Glam Beauty Portrait:, Beautiful Black woman, youthful spirit, creamy vanilla, silk press, mahogany red, subtle confidence, textured fabric, sapphire blue, minimal jewelry, beachside breeze, lens flare effect, nostalgic, cinematic lens, symmetrical composition, soft focus, high fashion photography, monochromatic, dewy finish, mysterious tension, layered elements
Luxury Glam Beauty Portrait:, Beautiful Black woman, youthful spirit, creamy vanilla, silk press, mahogany red, subtle confidence, textured fabric, sapphire blue, minimal jewelry, beachside breeze, lens flare effect, nostalgic, cinematic lens, symmetrical composition, soft focus, high fashion photography, monochromatic, dewy finish, mysterious tension, layered elements


A stunning 18-year-old Chinese girl with a youthful, pure face and realistic skin texture, sitting on a cozy, slightly messy bed in her bedroom. She is taking a mirror selfie with a smartphone, capturing a natural and intimate moment. Wearing casual gray loungewear and neat white crew socks. Soft natural light (golden hour) streams in from a side window, creating a warm, moody, and cinematic atmosphere. 35mm lens, sharp focus on the subject in the mirror, depth of field with a beautifully blurre
A stunning 18-year-old Chinese girl with a youthful, pure face and realistic skin texture, sitting on a cozy, slightly messy bed in her bedroom. She is taking a mirror selfie with a smartphone, capturing a natural and intimate moment. Wearing casual gray loungewear and neat white crew socks. Soft natural light (golden hour) streams in from a side window, creating a warm, moody, and cinematic atmosphere. 35mm lens, sharp focus on the subject in the mirror, depth of field with a beautifully blurred background (bokeh). Photorealistic, 8K, high resolution, studio quality, masterpiece. Negative Prompts: no extra limbs, no deformed hands, no blur, no noise, no watermark, no text, no cartoon/anime style. Aspect Ratio: 3:4.
I want you to act as a linux terminal. I will type commands and you will reply with what the terminal should show. I want you to only reply with the terminal output inside one unique code block, and nothing else. do not write explanations. do not type commands unless I instruct you to do so. when i need to tell you something in english, i will do so by putting text inside curly brackets {like this}. my first command is pwdI want you to act as an English translator, spelling corrector and improver. I will speak to you in any language and you will detect the language, translate it and answer in the corrected and improved version of my text, in English. I want you to replace my simplified A0-level words and sentences with more beautiful and elegant, upper level English words and sentences. Keep the meaning same, but make them more literary. I want you to only reply the correction, the improvements and nothing else, do not write explanations. My first sentence is "istanbulu cok seviyom burada olmak cok guzel"




9:16 的图片比例,生成一张抖音直播的截图,里面是 刘亦菲 在直播,刘亦菲 手里拿着牌子,牌子里写着 今晚直播,欢迎来参亦菲畅聊!
9:16 的图片比例,生成一张抖音直播的截图,里面是 刘亦菲 在直播,刘亦菲 手里拿着牌子,牌子里写着 今晚直播,欢迎来参亦菲畅聊!


生成一个抖音直播的截图 里面是一个美女在直播,在卖丝袜和内衣,她的在线人数是99996,热度是18+,有个叫小互的大哥,给她刷了一个飞机礼物
生成一个抖音直播的截图 里面是一个美女在直播,在卖丝袜和内衣,她的在线人数是99996,热度是18+,有个叫小互的大哥,给她刷了一个飞机礼物


9:16 的图片比例,生成一张抖音直播的截图,里面是 刘亦菲 在直播,刘亦菲 手里拿着牌子,牌子里写着 今晚直播,欢迎来参亦菲畅聊!
9:16 的图片比例,生成一张抖音直播的截图,里面是 刘亦菲 在直播,刘亦菲 手里拿着牌子,牌子里写着 今晚直播,欢迎来参亦菲畅聊!


35mm film photography with harsh convenience store fluorescent lighting mixed with colorful neon signs from outside, authentic film grain, high contrast, slight color cast, cinematic street editorial style, intimate medium shot, early 20s sexy Chinese female idol with ultra-realistic delicate refined Chinese features, seductive almond-shaped fox eyes with natural double eyelids, high nose bridge, small sharp V-shaped jawline, flawless porcelain skin with cool ivory undertone and visible specular
35mm film photography with harsh convenience store fluorescent lighting mixed with colorful neon signs from outside, authentic film grain, high contrast, slight color cast, cinematic street editorial style, intimate medium shot, early 20s sexy Chinese female idol with ultra-realistic delicate refined Chinese features, seductive almond-shaped fox eyes with natural double eyelids, high nose bridge, small sharp V-shaped jawline, flawless porcelain skin with cool ivory undertone and visible specular highlights from fluorescent light, subtle skin texture and micro pores, natural dewy makeup with soft flush on cheeks, glossy natural pink lips slightly parted, subtle natural freckles across nose and cheeks, long dark brown hair in a messy high ponytail with many loose strands falling around face and neck, wearing an oversized white button-up shirt as the only top, unbuttoned at the top with deep cleavage and loosely tied at the waist, paired with a tiny black pleated mini skirt, barefoot in simple white slides, seductive casual leaning pose against the glass door of a 24-hour convenience store at late night, body slightly arched, one leg bent with foot resting against the door frame, the other leg straight, one hand holding a bottle of iced drink, the other hand lightly pulling the hem of her mini skirt, intensely seductive playful yet slightly vulnerable gaze straight at the viewer with soft doe eyes full of quiet temptation and teasing smile, bright cold fluorescent store light from inside mixed with pink and blue neon glow from outside signs, realistic reflections on glass door, blurred convenience store interior with shelves and snacks in background, authentic 35mm film color grading with harsh lighting and neon accents, extremely sharp yet soft skin rendering, natural hair strands, realistic fabric wrinkles and drape on the oversized shirt and mini skirt, no plastic skin, no digital over-sharpening, no airbrushing, no blemishes, no moles, no oily skin, no watermark, no text, authentic late-night convenience store atmosphere


35mm film photography, warm vintage Japanese onsen ryokan aesthetic, soft ambient wooden lantern lighting mixed with gentle natural window light, subtle film grain, gentle color shift, high atmosphere editorial style, intimate medium shot, early 20s beautiful Chinese female idol with ultra-realistic delicate refined Chinese features, seductive almond-shaped fox eyes with natural double eyelids, high nose bridge, small sharp V-shaped jawline, flawless porcelain skin with warm ivory undertone, vis
35mm film photography, warm vintage Japanese onsen ryokan aesthetic, soft ambient wooden lantern lighting mixed with gentle natural window light, subtle film grain, gentle color shift, high atmosphere editorial style, intimate medium shot, early 20s beautiful Chinese female idol with ultra-realistic delicate refined Chinese features, seductive almond-shaped fox eyes with natural double eyelids, high nose bridge, small sharp V-shaped jawline, flawless porcelain skin with warm ivory undertone, visible subtle skin texture and micro pores, soft natural makeup with dewy glow, subtle rosy flush on cheeks, natural soft pink lips slightly parted, long dark brown hair tied in a loose low bun with some messy strands falling around face and neck, wearing a loose white yukata (traditional Japanese bathrobe) deliberately slipped off one shoulder and loosely tied at the waist, the fabric slightly open revealing smooth skin and subtle cleavage, barefoot, seductive relaxed sitting pose on the edge of a traditional wooden engawa veranda at a vintage onsen ryokan, body slightly turned toward the camera, one leg bent with foot resting on the wooden floor, the other leg gently dangling, one hand lightly holding the yukata collar, the other hand resting on the wooden floor behind her for support, softly arched back to gently accentuate curves, intensely seductive yet gentle and inviting gaze straight at the viewer with soft doe eyes full of quiet temptation and warmth, warm wooden interior with paper sliding doors and distant steaming hot spring in soft focus, gentle rim lighting highlighting skin and fabric texture, authentic vintage film color grading with warm tones, extremely sharp yet soft skin rendering, natural hair strands, realistic fabric wrinkles and drape on the yukata, no plastic skin, no digital over-sharpening, no airbrushing, no blemishes, no moles, no oily skin, no watermark, no text, authentic 35mm film Japanese onsen ryokan atmosphere


35mm color film photography with harsh direct on-camera flash, specular highlights on skin and clothing, strong catchlights in eyes, high contrast flash illumination, authentic film grain and color shift, high fashion fresh innocent basketball court editorial style, intimate first-person low-angle POV shot from below, early 20s sexy Chinese female idol with ultra-realistic delicate refined Chinese features, seductive almond-shaped fox eyes with natural double eyelids, high nose bridge, small sha
35mm color film photography with harsh direct on-camera flash, specular highlights on skin and clothing, strong catchlights in eyes, high contrast flash illumination, authentic film grain and color shift, high fashion fresh innocent basketball court editorial style, intimate first-person low-angle POV shot from below, early 20s sexy Chinese female idol with ultra-realistic delicate refined Chinese features, seductive almond-shaped fox eyes with natural double eyelids, high nose bridge, small sharp V-shaped jawline, flawless realistic porcelain skin with cool ivory undertone and visible flash specular highlights, fine delicate skin texture with subtle pores micro details and natural dewy glow under flash, fresh natural sporty makeup with soft dewy glow, subtle natural flush on cheeks, natural pink lips slightly parted, subtle natural freckles across nose and cheeks, long dark brown hair tied in a high playful ponytail with some loose strands framing the face and realistic loose strands, wearing a loose white tank top and white high-waisted basketball shorts, white knee-high sports socks, seductive natural leaning pose against the basketball hoop pole on the outdoor court at dusk, body angled sideways with naturally arched back and hips gently pushed back to accentuate perky round hips and sexy butt curve, one leg naturally extended forward toward the camera and the other leg slightly bent to emphasize long sexy legs, both hands lightly resting on the basketball pole at shoulder height, intensely seductive playful yet pitiable doe-eyed gaze straight at the viewer with soft vulnerable longing eyes and a gentle teasing smile full of quiet temptation and desire, harsh direct on-camera flash creating sharp specular highlights and strong catchlights, background with blurred basketball court and hoop under dusk sky, high contrast film color grading with natural flash look, extremely sharp yet soft skin rendering with authentic 35mm direct flash aesthetic, natural hair strands, realistic fabric texture on tank top and shorts with socks detail, no plastic skin, no digital over-sharpening, no airbrushing, no blemishes, no moles, no oily skin, no watermark, no text, authentic 35mm direct flash film basketball court look --ar 9:16



9:16 vertical — editorial portrait, single subject soft black mist filter, subtle haze, gentle highlight bloom, muted tones minimal indoor space, clean background, slight texture young Korean woman, minimal makeup, natural skin texture outfit: fitted ribbed knit top or soft camisole layered under a loose shirt, paired with high-waisted shorts or skirt; fabric slightly clings to body shape, soft and natural, no revealing elements hair: slightly messy, natural volume pose: sitting on floor with on
9:16 vertical — editorial portrait, single subject soft black mist filter, subtle haze, gentle highlight bloom, muted tones minimal indoor space, clean background, slight texture young Korean woman, minimal makeup, natural skin texture outfit: fitted ribbed knit top or soft camisole layered under a loose shirt, paired with high-waisted shorts or skirt; fabric slightly clings to body shape, soft and natural, no revealing elements hair: slightly messy, natural volume pose: sitting on floor with one leg bent and the other relaxed, body slightly leaning, shoulders not aligned, head tilted composition: subject slightly off-center, negative space present expression: calm, slightly distant, natural lips lighting: soft side light, gentle shadow falloff mood: understated, quiet, subtly sensual through natural body lines, relaxed and unposed quality: fine grain, slight softness, realistic look



9:16 vertical — Korean idol portrait photoshoot, 3x3 grid (nine frames), same person in all images, consistent facial features and styling. soft black mist filter effect, lowered contrast, blooming highlights, subtle glow around light sources, slightly faded blacks natural indoor setting near window, white curtains, clean wall background young Korean female idol, minimal makeup, soft realistic skin texture, slight imperfections outfit: white shirt + shorts, simple and relaxed styling hair: long
9:16 vertical — Korean idol portrait photoshoot, 3x3 grid (nine frames), same person in all images, consistent facial features and styling. soft black mist filter effect, lowered contrast, blooming highlights, subtle glow around light sources, slightly faded blacks natural indoor setting near window, white curtains, clean wall background young Korean female idol, minimal makeup, soft realistic skin texture, slight imperfections outfit: white shirt + shorts, simple and relaxed styling hair: long dark hair, slightly messy, naturally flowing poses vary across nine frames: standing, slight movement, seated, looking away, close-up, over-shoulder glance, candid transitions lighting: diffused daylight, soft shadows, gentle highlight bloom mood: quiet, intimate, everyday poetic moment, photobook aesthetic quality: ultra-realistic, subtle film grain, soft focus edges, dreamy atmosphere



Analog 35mm film photography, soft airy Japanese-style aesthetic, gentle diffused natural window light, slight overexposure, pastel tones, low contrast, soft highlights, minimal indoor setting near a window with white curtains, clean light-colored wall, natural composition, eye-level, slightly closer full-body framing (mid-thigh to head), young East Asian woman, natural minimal makeup, soft realistic skin texture, long slightly messy dark hair, oversized white button-up shirt, light casual short
Analog 35mm film photography, soft airy Japanese-style aesthetic, gentle diffused natural window light, slight overexposure, pastel tones, low contrast, soft highlights, minimal indoor setting near a window with white curtains, clean light-colored wall, natural composition, eye-level, slightly closer full-body framing (mid-thigh to head), young East Asian woman, natural minimal makeup, soft realistic skin texture, long slightly messy dark hair, oversized white button-up shirt, light casual shorts, barefoot, simple and relaxed styling, standing naturally with relaxed posture, arms loosely at sides or slightly behind, facing camera, gentle soft smile, subtle stillness, focus on light, air, and quiet everyday mood, soft film grain, dreamy and understated atmosphere --ar 9:16


生成一张竖版手机截图风格的图片,整体比例接近 9:16。画面中心偏上是一位真人 coser,扮演(角色名称)的二次元角色。人物为写实风格,但五官略带动漫感,皮肤细腻,眼睛稍大,表情温柔地看向镜头,坐在室内的休闲场景中,例如咖啡厅或酒吧吧台前,背景有符合场景的道具。画面最上方加入手机系统状态栏 UI,包括时间、电量、信号、网络等图标,让整张图看起来像手机截图。画面底部叠加一块宽大的半透明 galgame 风格对话框,对话框左侧放一个与画面人物对应的动漫或 Q 版头像;对话框右侧排版文字:第一行用较大字体显示与前面相同的角色名字,下面一到两行显示一段适合这个角色人设的、温柔治愈风格的简体中文台词,由你自动创作。再在对话框下方加一条操作栏,仿照 galgame UI。整体风格高清、细节丰富、光线柔和、二次元与真人写真自然融合。
生成一张竖版手机截图风格的图片,整体比例接近 9:16。画面中心偏上是一位真人 coser,扮演(角色名称)的二次元角色。人物为写实风格,但五官略带动漫感,皮肤细腻,眼睛稍大,表情温柔地看向镜头,坐在室内的休闲场景中,例如咖啡厅或酒吧吧台前,背景有符合场景的道具。画面最上方加入手机系统状态栏 UI,包括时间、电量、信号、网络等图标,让整张图看起来像手机截图。画面底部叠加一块宽大的半透明 galgame 风格对话框,对话框左侧放一个与画面人物对应的动漫或 Q 版头像;对话框右侧排版文字:第一行用较大字体显示与前面相同的角色名字,下面一到两行显示一段适合这个角色人设的、温柔治愈风格的简体中文台词,由你自动创作。再在对话框下方加一条操作栏,仿照 galgame UI。整体风格高清、细节丰富、光线柔和、二次元与真人写真自然融合。


该画面为中近景,采用平视镜头,聚焦于一位年轻女性。她以七分身镜头呈现,身体坐姿略带倾斜,臀部向后撅起,双腿自然交叠,左腿在前,右腿在后,膝盖微屈。她将上半身向右后方扭转,头部则转向镜头方向,形成一个经典的“回眸”姿态,目光直视镜头,眼神清澈而略带一丝俏皮。她的发型是蓬松的棕色齐肩短发,刘海自然垂落,发尾微卷,妆容清淡自然,仅在眼部有轻微眼线勾勒,唇色为自然裸粉。画面整体采用自然日光滤镜,光线从画面左上方斜射入,形成柔和的逆光轮廓,面部和身体右侧被温暖的金色光线照亮,左侧则形成自然的阴影过渡,增强了立体感。灯光效果是明亮的自然光,带有轻微的镜头眩光,营造出午后阳光的氛围。拍摄角度为平视,构图上,人物主体位于画面中偏右位置,背景中的斑马线与道路线条形成自然的引导线,将视线引向人物。背景为城市街道,包含道路、斑马线、绿化带和远处的车辆,背景被适度虚化,但依然可辨识出树木、护栏和停放的电动车等元素,构图上利用了三分法,人物位于右侧三分之一处,增强了画面的平衡感。主体穿着一件军绿色迷彩图案的连帽卫衣,下身搭配黑色短裤,脚穿白色高帮运动鞋配白色中筒袜。背包为黑色,带有橙黄色装饰条纹和一个橙色毛绒挂
该画面为中近景,采用平视镜头,聚焦于一位年轻女性。她以七分身镜头呈现,身体坐姿略带倾斜,臀部向后撅起,双腿自然交叠,左腿在前,右腿在后,膝盖微屈。她将上半身向右后方扭转,头部则转向镜头方向,形成一个经典的“回眸”姿态,目光直视镜头,眼神清澈而略带一丝俏皮。她的发型是蓬松的棕色齐肩短发,刘海自然垂落,发尾微卷,妆容清淡自然,仅在眼部有轻微眼线勾勒,唇色为自然裸粉。画面整体采用自然日光滤镜,光线从画面左上方斜射入,形成柔和的逆光轮廓,面部和身体右侧被温暖的金色光线照亮,左侧则形成自然的阴影过渡,增强了立体感。灯光效果是明亮的自然光,带有轻微的镜头眩光,营造出午后阳光的氛围。拍摄角度为平视,构图上,人物主体位于画面中偏右位置,背景中的斑马线与道路线条形成自然的引导线,将视线引向人物。背景为城市街道,包含道路、斑马线、绿化带和远处的车辆,背景被适度虚化,但依然可辨识出树木、护栏和停放的电动车等元素,构图上利用了三分法,人物位于右侧三分之一处,增强了画面的平衡感。主体穿着一件军绿色迷彩图案的连帽卫衣,下身搭配黑色短裤,脚穿白色高帮运动鞋配白色中筒袜。背包为黑色,带有橙黄色装饰条纹和一个橙色毛绒挂件,材质为帆布和皮革拼接。整体风格为街头休闲风,肢体语言放松自然,表情略带好奇与俏皮,整体呈现出一种随性、青春、充满活力的都市少女形象。



{ "prompt": { "style_and_tech": "mobile phone photo, old CCD camera aesthetic, harsh flash, grainy, dim messy indoor lighting, candid snapshot feeling, slight motion blur", "subject": "young Korean female idol, soft innocent look", "pose": "mid-action, slightly turning head toward camera as if just noticed being photographed, shoulders slightly raised", "expression": "eyes widened slightly, lips parted in surprise, shy and caught-off-guard expression", "clothing": "loose soft homewear (thin card
{ "prompt": { "style_and_tech": "mobile phone photo, old CCD camera aesthetic, harsh flash, grainy, dim messy indoor lighting, candid snapshot feeling, slight motion blur", "subject": "young Korean female idol, soft innocent look", "pose": "mid-action, slightly turning head toward camera as if just noticed being photographed, shoulders slightly raised", "expression": "eyes widened slightly, lips parted in surprise, shy and caught-off-guard expression", "clothing": "loose soft homewear (thin cardigan + inner top), slightly slipping off one shoulder but not revealing", "vibe": "unprepared, intimate, accidental moment, evokes curiosity and protectiveness", "aspect ratio":"9:16" } }


9:16 vertical — a 3x3 grid collage (nine images) forming a Korean idol portrait photoshoot series. Each frame features the same young Korean female idol, maintaining 100% consistency in facial features, proportions, hairstyle, and identity across all nine shots. Natural, ultra-realistic skin texture, no retouching, no smoothing. Clean idol-style minimal makeup, soft glow, subtle imperfections. Hair: long, voluminous dark hair, slightly tousled, consistent across all frames (natural loose flow, s
9:16 vertical — a 3x3 grid collage (nine images) forming a Korean idol portrait photoshoot series. Each frame features the same young Korean female idol, maintaining 100% consistency in facial features, proportions, hairstyle, and identity across all nine shots. Natural, ultra-realistic skin texture, no retouching, no smoothing. Clean idol-style minimal makeup, soft glow, subtle imperfections. Hair: long, voluminous dark hair, slightly tousled, consistent across all frames (natural loose flow, slight movement). Outfit: cohesive Korean idol photoshoot styling — white shirt + short bottoms (or simple neutral-toned outfit), youthful, clean, slightly casual but styled. Same outfit across all frames. Setting: minimal studio or simple indoor environment (plain wall, soft window light, clean background). Focus on subject, not environment. Lighting: soft diffused natural light, gentle highlights, low contrast, slightly airy tones, subtle film-like softness. Camera style: intimate portrait photography, slightly handheld feel, subtle imperfections (minor grain, slight blur in motion frames, imperfect framing). Frame breakdown (3x3 grid): Top row: - Top left: standing naturally, looking slightly away, relaxed expression - Top center: facing camera, casual mid-motion (hair or body slight movement) - Top right: slight side angle, soft gaze, natural candid feel Middle row: - Center left: looking slightly upward, soft thoughtful expression - Center: close-up portrait, direct eye contact, gentle idol smile - Center right: turning body slightly, mid-motion candid frame Bottom row: - Bottom left: seated or leaning casually, relaxed posture - Bottom center: back partially turned, looking over shoulder toward camera - Bottom right: standing close to frame, slightly playful or soft expression Mood: Korean idol photobook / photocard aesthetic, intimate, soft, natural, everyday charm. Quality: ultra-realistic, 8K detail, subtle analog film grain, natural imperfections, soft dreamy tone






1、生成特朗普和金正恩在抖音直播间打PK的截图 2、生成不知火舞的小红书主页截图 3、生成图片: 手写在教室黑板上的出师表全文,真实感的粉笔字迹,晴朗白天用iPhone手机实拍 4、生成图片: T-800机器人的淘宝商品详情页,展示: 机器人的正面侧面背面三视图, 产品价格, 产品细节, 功能和使用场景等
1、生成特朗普和金正恩在抖音直播间打PK的截图 2、生成不知火舞的小红书主页截图 3、生成图片: 手写在教室黑板上的出师表全文,真实感的粉笔字迹,晴朗白天用iPhone手机实拍 4、生成图片: T-800机器人的淘宝商品详情页,展示: 机器人的正面侧面背面三视图, 产品价格, 产品细节, 功能和使用场景等






1、生成不知火舞和貂蝉的游戏对战海报图 2、生成一张K-pop团体时尚专辑封面 3、请你生成 《斗破苍穹》 的关键人物关系图 4、帮我截一张上传图片的抖音首页的女网红图
1、生成不知火舞和貂蝉的游戏对战海报图 2、生成一张K-pop团体时尚专辑封面 3、请你生成 《斗破苍穹》 的关键人物关系图 4、帮我截一张上传图片的抖音首页的女网红图


Generate a cinematic minimal portrait of a solitary man standing in an intense orange to red gradient environment, strong silhouette lighting, deep shadow contrast, reflective glossy floor, symmetrical composition, minimal
Generate a cinematic minimal portrait of a solitary man standing in an intense orange to red gradient environment, strong silhouette lighting, deep shadow contrast, reflective glossy floor, symmetrical composition, minimal
I want you to act as an interviewer. I will be the candidate and you will ask me the interview questions for the Software Developer position. I want you to only reply as the interviewer. Do not write all the conversation at once. I want you to only do the interview with me. Ask me the questions and wait for my answers. Do not write explanations. Ask me the questions one by one like an interviewer does and wait for my answers.
My first sentence is "Hi"I want you to act as a javascript console. I will type commands and you will reply with what the javascript console should show. I want you to only reply with the terminal output inside one unique code block, and nothing else. do not write explanations. do not type commands unless I instruct you to do so. when i need to tell you something in english, i will do so by putting text inside curly brackets {like this}. my first command is console.log("Hello World");I want you to act as a text based excel. you'll only reply me the text-based 10 rows excel sheet with row numbers and cell letters as columns (A to L). First column header should be empty to reference row number. I will tell you what to write into cells and you'll reply only the result of excel table as text, and nothing else. Do not write explanations. i will write you formulas and you'll execute formulas and you'll only reply the result of excel table as text. First, reply me the empty sheet.
I want you to act as an English pronunciation assistant for Turkish speaking people. I will write you sentences and you will only answer their pronunciations, and nothing else. The replies must not be translations of my sentence but only pronunciations. Pronunciations should use Turkish alphabet letters for phonetics. Do not write explanations on replies. My first sentence is "how the weather is in Istanbul?"
I want you to act as a spoken English teacher and improver. I will speak to you in English and you will reply to me in English to practice my spoken English. I want you to keep your reply neat, limiting the reply to 100 words. I want you to strictly correct my grammar mistakes, typos, and factual errors. I want you to ask me a question in your reply. Now let's start practicing, you could ask me a question first. Remember, I want you to strictly correct my grammar mistakes, typos, and factual errors.
I want you to act as a travel guide. I will write you my location and you will suggest a place to visit near my location. In some cases, I will also give you the type of places I will visit. You will also suggest me places of similar type that are close to my first location. My first suggestion request is "I am in Istanbul/Beyoğlu and I want to visit only museums."
Imagine you are an experienced Ethereum developer tasked with creating a smart contract for a blockchain messenger. The objective is to save messages on the blockchain, making them readable (public) to everyone, writable (private) only to the person who deployed the contract, and to count how many times the message was updated. Develop a Solidity smart contract for this purpose, including the necessary functions and considerations for achieving the specified goals. Please provide the code and any relevant explanations to ensure a clear understanding of the implementation.