← Was Price
API
Tokens

Drive Was Price from your own code

The metered task is available over HTTP. A natural use is a price-tracking spreadsheet, a deals newsletter or a group-chat bot that already holds an item's price history: your code sends the deal, the printed 'was' price, the prices recorded before and the promo text, and gets back a verdict (buy_now, wait or skip), the honest discount against a price the store really charged, a rating of the 'was' price, every pressure tactic in the promo decoded, a target price to wait for and a walk-away price.

The split first: the page does the arithmetic; the model judges and writes; the page checks it. The facts (the lowest price recorded, the lowest in the 30 days before the sale, the 90-day median, how often the 'was' price was really charged, whether the price climbed in the 30 days before the sale, the pressure tactics found in the promo text) come from dealkit.js, which runs in node as well as in the browser. Its checkReply checks a verdict before it is shown: a discount that matches the page's own numbers, quotes that are really in the promo text, a target below today's price, a walk-away price that agrees with the verdict, and citations that exist. The thresholds are rules of thumb. Was Price never looks up a price anywhere: every price is one the caller supplies.

Base URL and the envelope

Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response uses one envelope:

{ "ok": true,  "data":  { ... } }
{ "ok": false, "error": { "code": "...", "message": "...", "status": 402, "details": { ... } } }

The token is minted for this app (the guest call takes {"slug":"was-price"}), so no slug header is needed afterwards. Send it as Authorization: Bearer …. The run body is the input object, sent bare: never wrap it as {"input": {…}}.

StatusCodeMeaning
400validation_errorThe body is not a JSON object, or task is missing.
401unauthorizedNo token, or a stale one. Mint a guest token or sign in again.
402insufficient_creditsThe balance is under min_credits. Price with /estimate first.
403forbiddenA guest token tried to run: the verdict task is metered and needs a personal token.
404not_foundUnknown job id.
429rate_limitedBack off and retry.
5xxserver_errorTransient. Retry with the same Idempotency-Key so a retry never double-bills.

The task field comes first

Every run sends task. There is one task, verdict; a missing or unknown task is treated as verdict.

taskDoesReply
verdictJudges whether one sale price is a real deal for this shopper, decodes the promo text and says what to do.verdict (buy_now - at or within 5% of the lowest recorded price with a real discount, under the cap, needed or wanted; wait - wanted, but it has sold for less, the price was raised before the sale, or the discount is only real against a padded reference; skip - the sale price is the everyday price, it is over the cap, or it is an impulse dressed up by pressure tactics), verdict_why, title, honest_discount {percent, against, note, cite} with against one of 30-day low, 90-day median, lowest recorded, claimed was price, anchor {rating, note, cite} with rating one of honest, stretched, fiction, unknown, tactics [{id, pressure, decoded, counter, cite}] (one per tactic the page found, plus up to two with id: "" and a verbatim quote), wait_plan {target_price, watch, recheck, cite} or null, walk_away_price, checks, caveats.

Cites are the fact ids in facts. The declared input schema carries scalars only, so object-valued fields travel JSON-encoded as strings (DealKit.toWire does this). Only task is required by the schema.

Input fields

FieldTypeNotes
taskstringverdict.
itemstringWhat is on sale, in the shopper's words; may be empty.
storestringWhere, in the shopper's words (for example online marketplace).
currencystringA symbol or code, e.g. $.
deal_pricenumberToday's price.
was_pricenumberThe printed reference price; 0 when there is none.
seenstringThe date the deal was seen, YYYY-MM-DD.
endsstringWhen the promo says it ends, in its own words; may be empty.
needstringneed, want or impulse.
capnumberThe most the shopper would pay; 0 when not set.
copystringThe promo text as shown; past 4,000 characters the middle is cut around a marker.
pricesstringThe earlier prices from the year before the deal, one per line: 2026-09-25 | 119.99 | online marketplace | note.
factsstring (JSON)[{id, text}] from DealKit.facts: F-deal, F-history, F-low, F-30d, F-median, F-anchor, F-cap, F-need, and F-hike, F-ends, F-sparse, F-nocopy and F-T1 … (one per pressure tactic) when they apply.
retry_notestringOnly on a reformat retry.

The reply

The job's output.output is a string holding one JSON object. Parse it with DealKit.parseReply, then normalize(reply), then checkReply(reply, deal), which returns {notes, analysis} - the notes (ok, warn, bad) that the page shows under "Checked against the ladder".

Worked examples

Replies produced against the app's prompt for the sample deals (items, stores and prices are made up). Between them they cover all three verdicts. The page replays each of them free from its sample buttons; the full replies ship in example-result.js. Long fields are folded.

task: verdict - Robot vacuum - buy_now

Input:

{
 "task": "verdict",
 "item": "Robot vacuum with mop pad",
 "store": "big-box store",
 "currency": "$",
 "deal_price": 199,
 "was_price": 349,
 "seen": "2026-10-19",
 "ends": "Sunday",
 "need": "need",
 "cap": 250,
 "copy": "Early Black Friday deal! Was $349, now $199.\nDeal ends Sunday. Free returns within 30 days.",
 "prices": "2026-04-18 | 349 | big-box store | launch price\n2026-05-30 | 299 | big-box store\n... 6 more lines",
 "facts": "[{\"id\": \"F-deal\", \"text\": \"Deal: $199 at big-box store, seen 2026-10-19; the store's 'was' price is $349 - 43% off as advertised.\"}, {\"id\": \"F-history\", \"text\": \"8 earlier prices recorded, 2026-04-18 to 2026-10-12 (5 in the 90 days before the deal, 3 in the last 30).\"}, {\"id\": \"F-low\", \"text\": \"Lowest price recorded: $229 on 2026-07-11 at online marketplace. The deal is $30 (13%) below it.\"}, \"... 9 more facts\"]"
}

Reply:

{
 "task": "verdict",
 "verdict": "buy_now",
 "verdict_why": "At $199 you're $30 below the lowest price ever recorded for this item and $51 under your $250 cap, and you marked it a need - that's as good as this item gets no matter what the 'was' price claims.",
 "title": "Robot vacuum with mop pad",
 "honest_discount": {
  "percent": 29,
  "against": "30-day low",
  "note": "Against the lowest price seen in the 30 days before the sale, $199 is a real 29% cut, well short of the store's claimed 43% off.",
  "cite": [
   "F-30d",
   "F-deal"
  ]
 },
 "anchor": {
  "rating": "stretched",
  "note": "The $349 'was' price only shows up once, at the launch price back in April, and never appears in the 90 days before this sale.",
  "cite": [
   "F-anchor"
  ]
 },
 "tactics": [
  {
   "id": "T1",
   "pressure": "medium",
   "decoded": "The countdown wants you to decide before Sunday instead of comparing prices. It doesn't change the fact that $199 is already below every price on record for this item.",
   "counter": "Decide on the price itself, not the clock - you don't need to rush a price this good.",
   "cite": [
    "F-T1",
    "F-deal"
   ]
  },
  {
   "id": "T2",
   "pressure": "high",
   "decoded": "Quoting $349 as the 'was' price wants $199 to look like a massive cut. That price was only charged once, at launch, and hasn't shown up again in the months before this sale.",
   "counter": "Judge the deal against the recent prices in the $270s-$290s, not the old $349 tag.",
   "cite": [
    "F-T2",
    "F-anchor"
   ]
  },
  "... 1 more"
 ],
 "wait_plan": null,
 "walk_away_price": 250,
 "checks": [
  "Confirm the robot vacuum with mop pad at checkout is the exact model behind the prices you've seen before, not a different variant.",
  "Check that the 'free returns within 30 days' mentioned in the promo really applies to this item before you buy.",
  "... 1 more"
 ],
 "caveats": [
  "The store's 43% off claim is measured against the $349 price, which was only charged once, months before this sale."
 ]
}

task: verdict - Air fryer - wait

Input:

{
 "task": "verdict",
 "item": "6-quart air fryer",
 "store": "online marketplace",
 "currency": "$",
 "deal_price": 89.99,
 "was_price": 159.99,
 "seen": "2026-10-19",
 "ends": "in 2 hours",
 "need": "want",
 "cap": 90,
 "copy": "Flash deal - ends in 02:14:33.\nOnly 4 left at this price! Compare at $159.99.\nEarly Black Friday pricing, 44% off.",
 "prices": "2025-11-28 | 79.99 | online marketplace | last Black Friday\n2026-01-10 | 99.99 | online marketplace\n... 6 more lines",
 "facts": "[{\"id\": \"F-deal\", \"text\": \"Deal: $89.99 at online marketplace, seen 2026-10-19; the store's 'was' price is $159.99 - 44% off as advertised.\"}, {\"id\": \"F-history\", \"text\": \"8 earlier prices recorded, 2025-11-28 to 2026-10-08 (4 in the 90 days before the deal, 2 in the last 30).\"}, {\"id\": \"F-low\", \"text\": \"Lowest price recorded: $79.99 on 2025-11-28 at online marketplace. The deal is $10 (13%) above it.\"}, \"... 11 more facts\"]"
}

Reply:

{
 "task": "verdict",
 "verdict": "wait",
 "verdict_why": "This air fryer sold for $79.99 as recently as last Black Friday, 13% below today's $89.99 price, and the price climbed 22% in the 30 days before this sale. The $159.99 'was' price was never actually charged, so the 44% off claim isn't measured against a real price.",
 "title": "6-Quart Air Fryer",
 "honest_discount": {
  "percent": 25,
  "against": "30-day low",
  "note": "Against the $119.99 low price from the 30 days before the sale, $89.99 is a real 25% discount.",
  "cite": [
   "F-30d"
  ]
 },
 "anchor": {
  "rating": "fiction",
  "note": "The $159.99 'was' price never shows up in the recorded prices - not in the 90 days before, and not in the year before.",
  "cite": [
   "F-anchor"
  ]
 },
 "tactics": [
  {
   "id": "T1",
   "pressure": "medium",
   "decoded": "The countdown wants you to decide before you have time to compare prices, as if the deal is about to vanish.",
   "counter": "Pause and check the recorded prices before the clock runs out.",
   "cite": [
    "F-T1"
   ]
  },
  {
   "id": "T2",
   "pressure": "medium",
   "decoded": "'Only 4 left' wants you to fear missing out, but there's no way to confirm the real stock count from here.",
   "counter": "Don't let a stock claim rush you - judge the price on its own.",
   "cite": [
    "F-T2"
   ]
  },
  "... 2 more"
 ],
 "wait_plan": {
  "target_price": 79.99,
  "watch": "Watch for the price to drop back to $79.99, which is what it sold for last Black Friday.",
  "recheck": "Check again when the real Black Friday sale this 'early' pricing is previewing arrives.",
  "cite": [
   "F-low",
   "F-hike",
   "F-30d"
  ]
 },
 "walk_away_price": 79.99,
 "checks": [
  "Make sure this is the same 6-quart size as the one that sold for $79.99 before.",
  "Check whether the 'only 4 left' stock count is something you can verify before paying.",
  "... 1 more"
 ],
 "caveats": [
  "Only 2 of the 8 recorded prices are from the last 30 days, so the 30-day low is based on limited history."
 ]
}

task: verdict - Smartwatch - skip

Input:

{
 "task": "verdict",
 "item": "Fitness smartwatch, 41 mm",
 "store": "the maker's own site",
 "currency": "$",
 "deal_price": 279,
 "was_price": 399,
 "seen": "2026-10-19",
 "ends": "today only",
 "need": "impulse",
 "cap": 200,
 "copy": "LOWEST PRICE OF THE YEAR! Compare at $399, now $279.\nFree sport band ($49 value) - today only. Selling fast.\nOr pay in 4 payments of $69.75.",
 "prices": "2026-05-14 | 279 | the maker's own site\n2026-06-21 | 279 | the maker's own site\n... 5 more lines",
 "facts": "[{\"id\": \"F-deal\", \"text\": \"Deal: $279 at the maker's own site, seen 2026-10-19; the store's 'was' price is $399 - 30% off as advertised.\"}, {\"id\": \"F-history\", \"text\": \"7 earlier prices recorded, 2026-05-14 to 2026-10-11 (4 in the 90 days before the deal, 2 in the last 30).\"}, {\"id\": \"F-low\", \"text\": \"Lowest price recorded: $259 on 2026-07-19 at online marketplace. The deal is $20 (8%) above it.\"}, \"... 12 more facts\"]"
}

Reply:

{
 "task": "verdict",
 "verdict": "skip",
 "verdict_why": "At $279 this is the price the maker's own site has charged repeatedly since May, not a special low, and it is $79 over your $200 cap. The $399 it is compared against was never actually charged, and you marked this as an impulse buy, so the pressure around it is not backed by a real deal.",
 "title": "Fitness smartwatch",
 "honest_discount": {
  "percent": 0,
  "against": "30-day low",
  "note": "The deal matches the lowest price charged in the 30 days before the sale, so there is no real discount against the most recent price the store actually used.",
  "cite": [
   "F-30d"
  ]
 },
 "anchor": {
  "rating": "fiction",
  "note": "The $399 'was' price was never recorded in the 90 days before the sale or anywhere in the year of history shown.",
  "cite": [
   "F-anchor"
  ]
 },
 "tactics": [
  {
   "id": "T1",
   "pressure": "medium",
   "decoded": "Wants a fast yes before you can compare; this same price has shown up repeatedly over the past several months, so 'today only' is about urgency, not a one-time price.",
   "counter": "Check the price history before deciding - a price that repeats often isn't a one-day event.",
   "cite": [
    "F-T1",
    "F-30d"
   ]
  },
  {
   "id": "T2",
   "pressure": "medium",
   "decoded": "Wants fear of missing out to skip the comparison step; nothing in the recorded facts can confirm stock is actually low.",
   "counter": "Don't let a stock claim rush the decision - weigh the price and your cap first.",
   "cite": [
    "F-T2"
   ]
  },
  "... 4 more"
 ],
 "wait_plan": null,
 "walk_away_price": 200,
 "checks": [
  "Confirm this listing is the same 41 mm model as the prices you've recorded, not a different size.",
  "Check the 4-payment plan for any added fee - the four payments alone total the full $279.",
  "... 1 more"
 ]
}

Build the input yourself

Load the script in node (global.window = {}; require("./dealkit.js")), then:

const K = DealKit;
const { deal } = K.parseDeal([
  "Item: 6-quart air fryer", "Deal: 89.99", "Was: 159.99", "Seen: 2026-10-19", "Need: want", "Cap: 90",
  "Promo: Flash deal - ends in 02:14:33. Compare at $159.99.",
  "2026-08-05 | 99.99 | online marketplace", "2026-10-08 | 129.99 | online marketplace"
].join("\n"));
const { input } = K.buildInput(deal);
const wire = K.toWire(input);        // POST this to /estimate and /run
// later: const n = K.normalize(K.parseReply(job.output.output));
// const { notes } = K.checkReply(n, deal);

Step 1 — a tiny client helper

One function that sends JSON and unwraps the envelope. Paste a personal token from /tokens.html, or leave it empty and mint a guest token in step 2.

BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="paste-your-token-here"   # from https://was-price.skillsafe.ai/tokens.html
api() { # api METHOD PATH [JSON-BODY]
  curl -s -X "$1" "$BASE$2" -H "Content-Type: application/json" \
    ${TOKEN:+-H "Authorization: Bearer $TOKEN"} ${3:+--data "$3"}
}

Step 2 — a token

A guest token is enough for /me and /estimate. A run is metered, so it needs a personal token: sign in on /tokens.html and copy it from there.

# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN=$(curl -s -X POST "$BASE/guest" -H "Content-Type: application/json" -d '{"slug":"was-price"}' | jq -r '.data.token')

Step 3 — GET /me

Who the token is (subject_type: user or guest) and the credit balance.

api GET /me | jq '.data | {subject_type, credits}' 

Step 4 — POST /estimate

Free. The body is the run input itself, the bare object (never wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model_alias reads gpt-terra.

# input.json: the run input object - {"task": "verdict", "item": "...", "deal_price": 89.99, "was_price": 159.99, "seen": "2026-10-19", "need": "want", "cap": 90, "copy": "...", "prices": "2026-09-25 | 119.99 | ...", "facts": "[...]"}
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}' 

Step 5 — POST /run, then poll the job

Send an Idempotency-Key built from the task and the input: a retry with the same key never bills twice. The page uses was-price:verdict:<inputHash>:a<attempt>, and its one reformat retry (with retry_note added) is attempt 2 - a new key and a second, named run. The job's output.output is the reply text; truncated: true means the balance cut it short.

KEY="was-price:verdict:$(shasum -a 256 input.json | cut -c1-16):a1"
JOB=$(curl -s -X POST "$BASE/run" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: $KEY" --data @input.json | jq -r '.data.job_id')
while :; do
  J=$(api GET "/jobs/$JOB"); S=$(echo "$J" | jq -r '.data.status')
  case "$S" in succeeded|failed|cancelled) break;; esac; sleep 2
done
echo "$J" | jq -r '.data.output.output' > reply.json   # one JSON object, the reply contract
echo "$J" | jq '.data | {charged_credits, truncated}' 

Step 6 — POST /run-stream

The same run as Server-Sent Events, with the same input.json. Browsers receive progress ticks rather than text deltas; the final done event carries the reply.

# Server-Sent Events: tick/delta events while the job runs, then a final event with the job
curl -N -s -X POST "$BASE/run-stream" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: was-price:verdict:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json