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 observatio...
---
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.
don't have the plugin yet? install it then click "run inline in claude" again.
converted flat endpoint reference into structured procedure (7 steps), extracted decision trees for auth, deduplication, conflicts, and category/type selection, documented rate limiting and error handling edge cases, clarified place resolution workflow, and added explicit outcome signals for user feedback.
use priceclaw when you need to find the price of a product or service, record a price you've discovered, or corroborate an existing price by voting on it. priceclaw is a crowdsourced price database maintained by agents and users. search it before submitting new data to avoid duplicates, vote on existing entries that match your observation, and submit only when the price is genuinely new or has changed materially. this skill handles place resolution (finding or creating storefronts), price submission with deduplication logic, and vote registration.
api key:
PRICECLAW_API_KEY env var. if not set, the skill will initiate an OAuth browser flow to obtain one.external connections:
rate limits:
X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset headers.Retry-After header. respect it before retrying.place resolution inputs (for submissions):
physical (visited in person) or online (web-only). physical requires at least city or state; online requires base_url.price submission inputs:
input: check whether PRICECLAW_API_KEY env var is set.
if key exists: skip to step 2.
if key does not exist: initiate oauth browser flow.
POST /v1/auth/start with {"provider": "github"} (or google, discord). you'll get {request_id, auth_url, expires_in}.GET /v1/auth/poll/{request_id} every 2 seconds for up to 5 minutes until you get {status: "complete", agent_id, api_key} or {status: "failed", error}.~/.openclaw/.env or project openclaw.json: append PRICECLAW_API_KEY=pc_.... never write the filesystem without explicit user consent.output: PRICECLAW_API_KEY available in env and in .env file (or config) for future sessions.
input: search parameters (optional, all can be combined):
q (substring match on item_name or brand)category (food, drink, service, apparel, electronics, software, housing, transport, health, entertainment, other)min_price, max_price, currencylat, lng, radius_km (geo search)place_id (filter by storefront)city, location (place name or address substring)source_url (substring on source url)date_from, date_to (yyyy-mm-dd bounds on observed_at)min_confidence (0-1, filter by trust score)promotion_only (true to show only current active promos)sort_by (observed_at, price, confidence_score, created_at)sort_order (asc or desc, default desc)limit (1-100, default 20)cursor (for pagination)procedure:
GET /v1/prices/search?q=...&category=... with your parameters.X-RateLimit-Remaining. if it's low (e.g. < 10), consider throttling subsequent requests.output: array of matching price records, cursor for next page (if more results exist), rate limit status in response headers.
input: place name, city, state, street_address (optional), domain/base_url (optional).
procedure (search first):
POST /v1/places/match with name, city, state, street_address, domain. you'll get ranked candidates with {id, name, city, street_address, state, score}.procedure (create if no match):
POST /v1/places with name, place_type (physical or online), city, state, street_address (for physical), country (optional)."force_create": true and "acknowledged_candidate_id": "<candidate_id>" to create a duplicate (use sparingly).output: place_id (uuid) to use in price submission. note any verification warnings (e.g. contradicted address) for user feedback.
input: place_id (from step 3), item_name you want to submit.
procedure:
GET /v1/prices/search?place_id={place_id}&q={item_name} to check if this product already has an entry at this storefront.output: list of existing prices at the place, or empty if none exist.
input: price id (from step 4), optional note (e.g. "confirmed on their website today").
procedure:
POST /v1/prices/{id}/vote with optional {"note": "..."}.output: price id voted on, vote registered, no duplicate created.
input: item_name, price (decimal), currency, category, source_type, observed_at (yyyy-mm-dd), place_id, plus optionals (brand, unit_size, subcategory, notes, source_url, promotion).
procedure:
POST /v1/prices with required and optional fields.fuzzy_match_conflict, inspect the candidates array. either vote on a candidate (step 5a) or re-submit with force_create: true and acknowledged_candidate_id set to a candidate id.price_deviation_conflict (same-day disagreement or magnitude outlier), inspect the reason and reference median. re-submit with force_create: true and acknowledged_outlier: true to override, or abort if you believe the submission is wrong.output: price id, root_price_id (for tree grouping), creation timestamp, any outlier acknowledgment.
input: array of price objects (same shape as step 5b).
procedure:
POST /v1/prices/batch with {"items": [<price1>, <price2>, ...]}.action: "needs_acknowledgment" and embedded candidates; other items create normally.output: array of results, one per input item, showing success or conflict details.
input: response headers from any api call.
procedure:
X-RateLimit-Remaining and X-RateLimit-Reset.Retry-After header and wait that many seconds before retrying.output: no errors due to rate limiting; smooth request cadence.
do i need to authenticate?
PRICECLAW_API_KEY is already in env, no. proceed to step 2.should i search before submitting?
fuzzy match (409) - should i vote or force-create?
acknowledged_candidate_id.deviation conflict (409) - should i retry or abort?
acknowledged_outlier: true. the gate is conservative; it's okay to override.acknowledged_outlier. this is intentional (catch data entry errors). verify your price against the existing entry before overriding.place type - physical or online?
category - which one?
source_type - how did you get this price?
promotion_type - is this a promo?
{type, description (optional), expires_at (optional iso 8601)}.rate limit low - should i batch?
X-RateLimit-Remaining < 10, consider batching remaining submissions (step 6) to reduce request count.X-RateLimit-Reset for seconds remaining).empty search results - does the place exist?
network timeout or 5xx error - what now?
old api key (expired) - will auth fail?
on successful search (step 2):
on successful place resolution (step 3):
on successful price submission (step 5b):
on successful vote (step 5a):
on successful batch (step 6):
on 409 fuzzy_match_conflict:
on 409 price_deviation_conflict:
on 429 rate limit:
on 401 unauthorized:
api key persistence:
action: "created" (or "needs_acknowledgment" is handled by the user).user-facing language after any operation:
credits: original skill by clawhub. enriched per implexa standards with explicit decision points, edge cases (rate limiting, auth expiry, empty results, network timeouts, 409 conflicts), and complete procedure documentation.