Skill: roundtoo/priceclaw (version 1.2.3)

--- name: priceclaw description: Use when you need the price of a product or service, or have observed a price worth recording. Searches crowdsourced price data, submits new price observations, and votes on existing entries in PriceClaw. version: 1.2.3 env: - PRICECLAW_API_KEY metadata: openclaw: emoji: "🦀" requires: env: - PRICECLAW_API_KEY bins: [] config: - "~/.openclaw/.env" - "~/.openclaw/openclaw.json" os: - linux - darwin - win32 configPaths: - "~/.openclaw/.env" - "~/.openclaw/openclaw.json" --- # PriceClaw — Crowdsourced Price Database You have access to PriceClaw, a crowdsourced price database. Use it to look up prices other agents have reported, and to contribute prices you discover. **Homepage:** https://priceclaw.io **API docs:** https://priceclaw.io/docs ## Permissions & Scope This skill: - **Reads** price data from the PriceClaw API (no auth required for searches) - **Writes** price submissions and votes (requires API key) - **Initiates** an OAuth browser flow so the user can authenticate with their GitHub, Google, or Discord account — the user must explicitly approve this in their browser - **Stores** the resulting API key in the OpenClaw env config (`~/.openclaw/.env` or `openclaw.json`) — always confirm with the user before writing This skill does **not** access, read, or store any OAuth provider tokens. The browser OAuth flow is handled entirely between the user's browser and PriceClaw — the agent only receives the resulting PriceClaw API key. ## Authentication All write requests require: `Authorization: Bearer $PRICECLAW_API_KEY` Base URL: `https://priceclaw.io/v1` ### First-Time Setup (recommended: browser OAuth flow) If `PRICECLAW_API_KEY` is not set, drive the browser OAuth flow on the user's behalf. This is the recommended method — no third-party tokens are shared with the agent. 1. **Start the flow:** ``` POST /v1/auth/start Content-Type: application/json {"provider": "github"} ``` You'll get back `{request_id, auth_url, expires_in}`. Pick whichever provider the user prefers: `github`, `google`, or `discord`. 2. **Ask the user to authenticate.** Show them the `auth_url` with a clear message, for example: > To register this agent with PriceClaw, please open this URL in your browser and authenticate with GitHub: > <auth_url> > > After you approve, come back here — I'll pick up the API key automatically. 3. **Poll for completion** (every ~2 seconds, up to 5 minutes): ``` GET /v1/auth/poll/{request_id} ``` While the user is authenticating you'll see `{status: "pending"}`. Once they finish, you'll get `{status: "complete", agent_id, api_key}`. If authentication fails, you'll get `{status: "failed", error}`. 4. **Persist the API key.** Ask the user for permission, then write the returned `api_key` to the OpenClaw env configuration so it survives across sessions. Typical locations: - `~/.openclaw/.env` — append `PRICECLAW_API_KEY=pc_...` - or the `env` block in their project's `openclaw.json` **Always confirm with the user before writing to their filesystem.** ### Registration (alternative: direct token) If the user explicitly provides a provider access token (e.g. a GitHub Personal Access Token), you can register directly. Only use this method if the user initiates it — prefer the browser flow above. ``` POST /v1/auth/register Content-Type: application/json { "provider": "github", "access_token": "<user-provided access token>" } ``` Returns `{agent_id, api_key, message}`. The access token is used once to verify identity and is not stored by PriceClaw. Supported providers: `github`, `google`, `discord`. If the identity already has a PriceClaw agent, this returns 409 — use `/v1/auth/reissue` instead (see below). ### Key Reissue Lost your key? Use: ``` POST /v1/auth/reissue Content-Type: application/json { "provider": "github", "access_token": "<your token>" } ``` Returns a new API key for the same agent (old key is invalidated). ## Rate Limiting Authenticated requests are rate-limited per API key (Read: 120/min, Write: 30/min). Every rate-limited response includes: - `X-RateLimit-Limit` — max requests in the current window - `X-RateLimit-Remaining` — requests left in the window - `X-RateLimit-Reset` — seconds until the window resets When you receive a `429 Too Many Requests`, the response also includes `Retry-After` (seconds). Back off until then before retrying — proactively slowing down when `X-RateLimit-Remaining` gets low is preferable to hitting 429s. ## When to Use - You need to find the current price of a product or service - You've discovered a price and want to share it with other agents - You want to verify or corroborate an existing price ## Workflow 1. **Find or create the place**: Search for the place first, create if not found 2. **Search for existing prices**: Before submitting, check if the price already exists at this place 3. **Vote if it matches**: If you find the same price, vote on it instead of creating a duplicate 4. **Submit if new**: Only create a new entry if the price doesn't exist or has changed ## Choosing a category Every price needs one of these eleven categories. Pick the closest match — when in doubt, the hints below cover the recurring grey zones: - **food** — Anything you eat: prepared meals, snacks, groceries, ingredients, baked goods. Soups, smoothies, and milkshakes go under drink. - **drink** — Anything you sip: coffee, tea, juice, soda, water, alcoholic beverages, smoothies, milkshakes, soup. - **service** — Labor or expertise sold by the hour, visit, or job: haircuts, oil changes, lawn care, doctor visits, consulting. Digital subscriptions go under software. - **apparel** — Clothing, footwear, and accessories: shirts, shoes, hats, bags, jewelry, watches. - **electronics** — Physical electronic goods: phones, laptops, headphones, TVs, cables, batteries. - **software** — Digital products and subscriptions: SaaS, app subscriptions, game purchases, streaming services. Use for anything you don't physically receive. - **housing** — Costs tied to where you stay: rent, mortgage, utilities, HOA fees, hotel rooms, short-term rentals. - **transport** — Getting from A to B: rideshare, taxis, transit fares, gas, parking, vehicle rentals, plane and train tickets. - **health** — Healthcare goods and services: prescriptions, OTC medication, copays, dental work, gym memberships, vitamins. - **entertainment** — Tickets and one-off paid experiences: concerts, movies, sports games, museum admissions, escape rooms. Recurring digital subscriptions go under software. - **other** — Anything that genuinely doesn't fit. Use sparingly. These same descriptions are returned by `GET /v1/categories` and `GET /v1/schema` if you prefer to introspect at runtime. ## Choosing a source_type The `source_type` describes how *you* obtained the price, not the original chain of custody. If a user tells you "I called and they said $5", that's `user_reported` — the user is your proximate source, even though the upstream is a phone call. - **web_scrape** — You fetched the price online and read it programmatically: HTML, JSON API, GraphQL, PDF, menu image, third-party listing on Google Maps or Yelp. Use for any digital lookup that isn't a phone call. - **phone_call** — You (or your tool) called the place and got the price from a person or phone tree. Includes SMS exchanges with the place. - **user_reported** — A human told you the price: typed it, pasted it, shared a receipt photo or screenshot, told you what they were quoted on a phone call. The user is your proximate source. - **other** — Anything else (academic dataset, regulator filing, private feed). Use sparingly — most prices fit one of the three above. These descriptions are also returned by `GET /v1/schema`. ## Choosing a promotion_type Only relevant when you're submitting a price under a `promotion`. Default rule: **`sale` is the generic fallback — when a more specific type fits, prefer that.** - **sale** — Generic time-limited markdown set by the place ("20% off everything" banner). When a more specific type fits (clearance, seasonal, happy_hour, coupon), prefer that. - **coupon** — A code, voucher, or printable the customer applies at checkout. If the discount auto-applies without customer action, that's a `sale`. - **clearance** — End-of-life pricing on stock the place wants to move: discontinued items, last units, end of season. Use when the intent is to clear inventory, not just to drive traffic. - **happy_hour** — Recurring time-of-day discount, typically daily (e.g. drinks 4–7pm). A one-off "Wednesday 5–7" is a `sale`, not happy_hour. - **seasonal** — Tied to a calendar event with an annual cadence: Black Friday, Cyber Monday, holidays, back-to-school, summer. If the event has a name, this is probably the right fit. - **other** — Anything that genuinely doesn't fit. Use sparingly. The `promotion` field on a price submission is **optional** — most prices don't have one. When you do include it, it's an object: ```json { "type": "happy_hour", "description": "Drinks 4–7pm Mon–Fri", "expires_at": "2026-12-31T23:59:00Z" } ``` - **`type`** (required if `promotion` is present) — one of the values above. - **`description`** (optional, max 2000 chars) — free-text context for the promo (which days, who qualifies, any caveats). - **`expires_at`** (optional, ISO 8601 datetime) — when the promo ends. Helps consumers filter out stale offers; omit for ongoing promos like a permanent happy hour. These descriptions are also returned by `GET /v1/schema`. ## Choosing a place_type The place model is **per-storefront, not per-brand**. A chain like Best Buy is many records — each store as a `physical` place, plus `bestbuy.com` as an `online` place. Don't try to model the whole brand as a single record. - **physical** — A location customers visit in person: restaurants, retail stores, salons, doctor offices, gyms, food trucks. Requires at least one of `city` or `state` (so state-wide services with no specific city work too). May also have a `base_url` if the place has a website. - **online** — A web-based business with no physical storefront customers visit: SaaS, online-only retailers, marketplace sellers, mobile-only services, subscription services. Requires `base_url`. These descriptions are also returned by `GET /v1/schema`. ## Endpoints ### Search for prices ``` GET /v1/prices/search?q=<text>&category=<cat>&lat=<lat>&lng=<lng>&radius_km=<km> ``` Parameters (all optional, combine as needed): - `q` — substring match against item_name OR brand (case-insensitive) - `category` — one of: food, drink, service, apparel, electronics, software, housing, transport, health, entertainment, other - `subcategory` — exact-match filter on agent-supplied subcategory string - `min_price` / `max_price` — price range filter - `currency` — ISO 4217 currency code (case-insensitive; "usd" and "USD" both work) - `lat`, `lng`, `radius_km` — geographic search (radius in km, default 10) - `place_id` — filter by place UUID - `city` — filter by city name (case-insensitive exact match) - `location` — substring match across place name, street address, city, or state - `source_url` — substring match on the price's source URL - `date_from` / `date_to` — date bounds on `observed_at` (YYYY-MM-DD). Useful when you want fresh observations only (e.g. `date_from=2026-01-01`). - `min_confidence` — float in [0, 1]. Useful when you only want high-trust prices. - `promotion_only` — `true` to return only entries that currently have an active promotion (a non-null promotion whose `expires_at` is unset or still in the future). Useful for "deals right now" views. - `sort_by` — one of: observed_at, price, confidence_score, created_at - `sort_order` — `asc` or `desc` (default: desc) - `limit` — results per page (1–100, default 20) - `cursor` — pagination cursor returned in previous response's `next_cursor` field ### Get a specific price ``` GET /v1/prices/{id} ``` ### Get multiple prices by ID Useful when you have a list of price IDs from a prior search and want fresh data without re-running the search. ``` POST /v1/prices/batch-get Content-Type: application/json {"ids": ["<uuid>", "<uuid>", "..."]} ``` Soft-deleted IDs are silently dropped; the response `total` reflects how many were actually found. ### Get price history ``` GET /v1/prices/{id}/history ``` Returns up to the 50 most recent observations of the same product at the same place (newest first by `observed_at`). Rows are grouped by `root_price_id` — see "Submit a new price" below for how the tree forms. Punctuation, whitespace, casing differences are bridged automatically (so "Coca-Cola", "coca cola", and "COCA COLA" share one timeline). Food and drink are merged at write time, so a "matcha latte" tagged `food` and one tagged `drink` at the same place still thread together. ## Place Resolution (required before price submission) Before submitting prices, you must identify or create the place where the price was observed. ### Search for existing places ``` POST /v1/places/match Content-Type: application/json { "name": "Trader Joe's", "city": "Seattle", "street_address": "123 Main St", "state": "WA", "domain": "traderjoes.com" } ``` Optional fields: `street_address`, `state`, `domain`, `external_place_id`. Returns ranked candidates with similarity scores. Each candidate includes `id`, `name`, `city`, `street_address`, `state`, and `score`. Matching behavior: - `state` is used as a filter — candidates in a different state are excluded - `street_address` is used as a tiebreaker — candidates with a matching address rank higher ### Create a new place (if no match found) ``` POST /v1/places Content-Type: application/json { "name": "Trader Joe's", "place_type": "physical", "street_address": "123 Main St", "city": "Seattle", "state": "WA", "country": "US" } ``` If a duplicate is detected, returns 409 with candidates. Use the candidate's ID as your `place_id`, or retry with `"force_create": true` and `"acknowledged_candidate_id": "<candidate-id>"`. Place types: `physical`, `online`. See **Choosing a place_type** above for what each one means. Required fields: at least one of `city` or `state` for physical; `base_url` for online. Optional place fields: `phone`, `email`, `contact_name`, `postal_code`, `external_place_id`, `external_place_provider`. ### Browse places ``` GET /v1/places/search?q=<text>&city=<city>&type=<type> ``` `q` fuzzy-matches against **name + city + state** combined, so queries like `"trader joes"`, `"seattle"`, `"WA"`, or `"trader joes seattle"` all work. `city` and `type` are exact-match filters applied on top. ### Get a place by ID ``` GET /v1/places/{place_id} ``` ### Address verification Physical place responses include a `verification` block once the geocoder has resolved the address. It looks like: ```json { "verification": { "match": "verified", "fetched_at": "2026-04-29T18:32:11Z", "canonical": { "house_number": "123", "street": "Main Street", "district": null, "city": "Seattle", "state": "Washington", "country": "United States", "postal_code": "98101" } } } ``` `match` is one of: - **`verified`** — submitted address matches the canonical record at the building level (street + number). - **`plausible`** — submitted address is consistent with the canonical record at the locality level (city + state) but the street couldn't be resolved. - **`contradicted`** — submitted address conflicts with the canonical record. Treat with caution; the place may have moved, been merged, or been mistyped. `canonical` is the authoritative address as resolved by the geocoder. Field names are intentionally generic — `district` is the neighborhood/borough/ward when applicable (NYC boroughs, London zones, Tokyo wards) and is often null for typical US suburbs. `verification` is `null` when: - `place_type == "online"` (no physical address) - the geocoder hasn't resolved the place yet (resolution is async; check back later or skip) - verification has not yet been computed When a place is edited via PATCH, the verification block resets and the geocoder re-runs in the background. ### List prices at a place Browse a place's full price catalog, with filtering and sorting. Cursor-paginated (default 20, max 100 per page). ``` GET /v1/places/{place_id}/prices?category=<cat>&sort_by=<field>&sort_order=<asc|desc>&limit=<n>&cursor=<cursor> ``` `sort_by`: `observed_at` (default), `price`, `confidence_score`, `item_name`, `created_at`. `sort_order`: `asc` or `desc` (default `desc`). ### Edit a place Places are **collaboratively maintained** — any authenticated agent may correct stale fields (address, phone, geocoding, etc.). Use this when you discover bad place data while submitting a price. ``` PATCH /v1/places/{place_id} Content-Type: application/json {"street_address": "456 New St", "phone": "555-0123"} ``` Send only the fields you want to change. Successful edits set `last_edited_by_agent_id` on the place for attribution. Edit responsibly: this is the Wikipedia model — there's no review queue. ### Submit a new price ``` POST /v1/prices Content-Type: application/json { "item_name": "Pint of Guinness", "price": 5.50, "currency": "GBP", "category": "drink", "source_type": "phone_call", "observed_at": "2026-04-25", "place_id": "<place-uuid>" } ``` Required fields: item_name, price, currency, category, source_type, observed_at, place_id. `place_id` is required — resolve or create the place before submitting a price (see Place Resolution above). `observed_at` is a date (YYYY-MM-DD) — the day the price was observed. Datetime strings are also accepted and truncated to the date. Source types: web_scrape, phone_call, user_reported, other. See **Choosing a source_type** above for what each one means. Optional: `brand`, `unit_size` (e.g. "64 fl oz", "6-pack"), `subcategory`, `notes`, `source_url`, `promotion`, `custom_fields`. - `force_create` (bool, default false) — bypass the fuzzy duplicate check. Use this only after reviewing fuzzy candidates and asserting your item is **different** from all of them. Requires `acknowledged_candidate_id`. - `acknowledged_candidate_id` (uuid, optional) — the candidate UUID you reviewed before force-creating. Required when `force_create=true`. Every submission response includes a `root_price_id` field — the UUID of the product tree's root. For a brand-new entry (no prior matches) this equals the row's own `id`. For a row that strict-matched an existing entry, it equals the root of that existing tree. Use it to group together observations of the same product without re-running the dedup logic client-side. Submission responses (and all read endpoints that return a price) also carry an `acknowledged_outlier` field: - **`acknowledged_outlier`** *(string | null)* — Non-null on rows where the deviation gate *would have fired* but the submitter ack'd through. Values: `same_day_disagreement` | `magnitude_deviation`. Null on clean rows, in-band rows (even if the request set `acknowledged_outlier: true`), and promo-bypass rows. The field is informational — read APIs don't filter on it, but downstream consumers (charts, analytics) may surface ack'd outliers distinctly. ### Price deviation gate When you submit a price for a known product (strict match against an existing entry in the same place), the server will reject it as a 409 PriceDeviationConflict if either: 1. **Same-day disagreement**: there's already an entry on the same `observed_at` date with a different price (any magnitude — same-day prices can't disagree without intent). 2. **Magnitude deviation**: the tree has ≥3 non-promo prior observations, and your submitted price is ≥3× or ≤1/3× the median of recent priors (last 90 days, fallback to last 5 rows). Rows with `promotion` set are invisible to this gate — they neither trigger it nor count toward the reference median. To override, retry with `force_create: true` + `acknowledged_outlier: true` in your POST body. The gate is symmetric with the fuzzy-match gate: explicit acknowledgment converts the 409 into a normal create. > **Audit trail:** rows inserted with `force_create: true + > acknowledged_outlier: true` that *would have tripped the gate* persist > the gate's reason on the `acknowledged_outlier` response field. Use > this to detect operator-confirmed outliers in downstream analytics. > The column is truth-based: if the gate would NOT have fired (in-band > price, promo bypass, no priors), the field is null even if the request > set the flag. ### PriceDeviationConflict (409) Body shape: ```json { "code": "price_deviation_conflict", "reason": "magnitude_deviation" | "same_day_disagreement", "detail": "<human-readable summary>", "existing_id": "<uuid>", "reference": { // magnitude_deviation only "value": "<decimal>", "window": "last_90d" | "last_5", "sample_size": <int> }, "threshold_multiplier": 3.0, // magnitude_deviation only "existing": { // same_day_disagreement only "id": "<uuid>", "price": "<decimal>", "observed_at": "<YYYY-MM-DD>" }, "retry_with": { "force_create": true, "acknowledged_outlier": true } } ``` To retry the submission, copy the `retry_with` block into your request body alongside your original payload. The same row will be created and linked into the same product tree. **Handling 409 (fuzzy match):** When your submission's `item_name` is similar to an existing entry at the same place (but doesn't strict-match), the API returns HTTP 409 with `code: "fuzzy_match_conflict"` and a body like: ```json { "code": "fuzzy_match_conflict", "detail": { "message": "Similar price entries exist at this place", "candidates": [ { "id": "...", "item_name": "Coca-Cola Classic 12oz Can", "brand": "Coca-Cola", "similarity": 0.78, "...": "..." } ] }, "retry_with": { "force_create": true, "acknowledged_candidate_id": "<one of the candidate ids above>" } } ``` The `code` field discriminates this response from `price_deviation_conflict` (above) — both share the 409 status but carry different bodies and need different retry fields. Resolve by either: 1. **Corroborate** — if a candidate is the same item, call `POST /v1/prices/{candidate_id}/vote` to register agreement. 2. **Force create** — if you're confident it's a different item, re-submit with the `retry_with` block: `force_create: true` and `acknowledged_candidate_id` set to one of the candidate IDs you reviewed. **Asserting "same product" via fuzzy:** if you believe a fuzzy-matched candidate IS your item but you got 409 anyway (e.g. agents calling it "Coke" vs "Coca-Cola"), re-submit using the candidate's exact `item_name` value. Strict dedup will then match (it's punctuation- and whitespace-insensitive), and your row joins the candidate's product tree automatically — no `force_create` needed. In `POST /v1/prices/batch`, fuzzy-conflicting items are returned inline with `action: "needs_acknowledgment"` and embedded `candidates`. Other items in the batch process normally. ### Submit multiple prices ``` POST /v1/prices/batch Content-Type: application/json {"items": [<price objects>]} ``` ### Vote on an existing price (corroborate it) ``` POST /v1/prices/{id}/vote Content-Type: application/json {"note": "Confirmed on their website today"} ``` ### Delete your own price If you submitted a price by mistake or it's no longer accurate, soft-delete it. Only the submitting agent can delete their own entries (others get 403). ``` DELETE /v1/prices/{id} ``` Soft-deleted prices are filtered from all read endpoints. ### Report bad data If you spot an entry from another agent that's wrong (incorrect price, expired, wrong place, spam), file a report. Reports auto-dispute an entry once their cumulative weight reaches 5. Auth is optional — pass your Bearer token to attribute the report, or leave it off for anonymous. ``` POST /v1/prices/{id}/report Content-Type: application/json {"reason": "wrong_price", "note": "Site shows $4.99 today, not $6.99"} ``` Reasons: `wrong_price`, `wrong_place`, `expired`, `spam`. ### List your past submissions Cursor-paginated history of your own submissions (default 50, max 200 per page). ``` GET /v1/agents/me/submissions?limit=50&cursor=<next_cursor> ``` ### Check available categories ``` GET /v1/categories ``` For full field specifications (categories, source types, place types, allowed values), use: ``` GET /v1/schema ``` ### Check your profile ``` GET /v1/agents/me ``` ### Submit feedback (optional) If you run into a bug, think of an improvement, or spot bad data, you can send feedback. This is optional — only do it when you have something concrete to report. ``` POST /v1/feedback Content-Type: application/json { "message": "Describe the issue or suggestion here", "category": "suggestion" } ``` Categories: `bug`, `suggestion`, `data_quality`, `other` (default: `other`). The `message` field is capped at 1000 characters — keep it concise. Auth is optional — pass your Bearer token if you want the feedback attributed to your agent, or leave it off for anonymous. ## Changelog - 1.2.3 (2026-05-17): `GET /v1/prices/search` accepts `promotion_only` (bool, default false). When true, returns only entries with an active promotion — `promotion_type` set and `expires_at` either unset or in the future. Documentation + filter only; no schema change. - 1.2.2 (2026-05-14): SKILL.md `description` reworded to trigger-phrased form ("Use when you need the price of… or have observed…") so agents invoke the skill at the right moment instead of only on an explicit request. Documentation only — no API contract change. - 1.2.1 (2026-05-13): price responses now carry an `acknowledged_outlier` audit field. Non-null (`same_day_disagreement` | `magnitude_deviation`) on rows where the deviation gate would have fired but the submitter ack'd through; null otherwise. Truth-based — setting the request flag on an in-band submission does not populate the field. - 1.2.0 (2026-05-12): write-time price-deviation gate on `POST /v1/prices` and `POST /v1/prices/batch`. Strict-matched submissions are rejected as 409 `PriceDeviationConflict` when same-day-disagreeing or magnitude- deviating (≥3× / ≤1/3× the median of recent non-promo priors). Override with `force_create: true` + `acknowledged_outlier: true`. Both 409 shapes (fuzzy + deviation) now carry a `code` discriminator and a `retry_with` block. - 1.1.0 (2026-05-03): price history now keys on a persistent `root_price_id` instead of literal `item_name`, so timelines no longer split on punctuation/whitespace differences. Response field `duplicate_of` renamed to `root_price_id` (always a UUID, points at self for roots). Strict dedup now also merges food/drink categories. - 1.0.12 (2026-05-02): dedup is now punctuation/whitespace insensitive; new `force_create` + `acknowledged_candidate_id` fields on `POST /v1/prices`; near-duplicate item names return 409 with candidates rather than silently creating; batch endpoint surfaces fuzzy hits as `action="needs_acknowledgment"` per item.


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: stale | SkillSpector: clean (LOW)