Cookbook
Recipe: categorise product listings
Pick a category from twelve, check the item against your prohibited list and pick its condition, then publish the listing with its fields or hold it for a person.
A seller lists a used city bike. One request picks its category from twelve, checks it against your prohibited items and reads its condition. The listing goes live with the right fields, or waits for a person when the item might be prohibited.
The request
- State. The listing as JSON. The questions point at the title and the description; the model still reads the price and the seller type next to them.
categoryis apickof 12 options, withotheras the way out. The descriptions separate close neighbours:bikesholds complete bicycles,bike_partsholds parts and accessories, andsportsholds everything but bicycles. Every description adds question tokens: this request has 216 question tokens against 73 for the state.prohibitedis acheckwhosecriterialist the prohibited kinds: weapons, drugs, counterfeit goods, stolen goods, live animals and recalled products. Keep that list in one place in your code and send it with every request.conditionis apickof four conditions a buyer understands.min_confidenceon every question: 0.6 on the prohibited check, where a wrong answer costs most.
{
"state": {
"listing": {
"title": "Gazelle city bike, 57 cm frame",
"description": "Dutch city bike with 3 gears and a front basket. Rides well, but the rear light is broken and the chain has some rust. Collect in Utrecht.",
"price_eur": 140,
"seller_type": "private"
}
},
"questions": {
"category": {
"type": "pick",
"instructions": "Which category fits {{listing.title}} with {{listing.description}} best?",
"options": {
"bikes": "Complete bicycles and e-bikes",
"bike_parts": "Bicycle parts and accessories",
"cars": "Cars, motorbikes and scooters",
"electronics": "Computers, audio, cameras and consoles",
"phones": "Phones, tablets and smartwatches",
"home_garden": "Furniture, household items and garden",
"fashion": "Clothes, shoes and bags",
"sports": "Sports and outdoor gear other than bicycles",
"toys": "Toys and baby items",
"books_media": "Books, music, films and games",
"tickets": "Tickets and vouchers",
"other": null
},
"min_confidence": 0.5
},
"prohibited": {
"type": "check",
"instructions": "Is the item in {{listing.description}} prohibited on a general marketplace?",
"criteria": "Prohibited: weapons, drugs, counterfeit goods, stolen goods, live animals and recalled products.",
"min_confidence": 0.6
},
"condition": {
"type": "pick",
"instructions": "What condition is the item in {{listing.description}} in?",
"options": {
"new": "Unused, in its original packaging",
"as_new": "Used briefly, no visible wear",
"used": "Normal signs of use, works",
"for_parts": "Broken or only good for parts"
},
"min_confidence": 0.5
}
}
}
Run it
Put a test key in DEX_API_KEY (see the Quickstart). Save the Python code as categorisation.py and run python categorisation.py. Save the TypeScript code as categorisation.mts and run npx tsx categorisation.mts: the code uses await at the top level, and the .mts ending makes the file an ES module. Install the SDKs from Downloads.
curl https://api.thinqit.ai/v1/decide \
-H "authorization: Bearer $DEX_API_KEY" \
-H "content-type: application/json" \
--data-binary @- <<'DEX_REQUEST'
{
"state": {
"listing": {
"title": "Gazelle city bike, 57 cm frame",
"description": "Dutch city bike with 3 gears and a front basket. Rides well, but the rear light is broken and the chain has some rust. Collect in Utrecht.",
"price_eur": 140,
"seller_type": "private"
}
},
"questions": {
"category": {
"type": "pick",
"instructions": "Which category fits {{listing.title}} with {{listing.description}} best?",
"options": {
"bikes": "Complete bicycles and e-bikes",
"bike_parts": "Bicycle parts and accessories",
"cars": "Cars, motorbikes and scooters",
"electronics": "Computers, audio, cameras and consoles",
"phones": "Phones, tablets and smartwatches",
"home_garden": "Furniture, household items and garden",
"fashion": "Clothes, shoes and bags",
"sports": "Sports and outdoor gear other than bicycles",
"toys": "Toys and baby items",
"books_media": "Books, music, films and games",
"tickets": "Tickets and vouchers",
"other": null
},
"min_confidence": 0.5
},
"prohibited": {
"type": "check",
"instructions": "Is the item in {{listing.description}} prohibited on a general marketplace?",
"criteria": "Prohibited: weapons, drugs, counterfeit goods, stolen goods, live animals and recalled products.",
"min_confidence": 0.6
},
"condition": {
"type": "pick",
"instructions": "What condition is the item in {{listing.description}} in?",
"options": {
"new": "Unused, in its original packaging",
"as_new": "Used briefly, no visible wear",
"used": "Normal signs of use, works",
"for_parts": "Broken or only good for parts"
},
"min_confidence": 0.5
}
}
}
DEX_REQUEST# Save as dex_request.py, then run: python dex_request.py
import json
from thinqit_dex import Client
client = Client() # reads DEX_API_KEY from the environment
request = json.loads(r'''
{
"state": {
"listing": {
"title": "Gazelle city bike, 57 cm frame",
"description": "Dutch city bike with 3 gears and a front basket. Rides well, but the rear light is broken and the chain has some rust. Collect in Utrecht.",
"price_eur": 140,
"seller_type": "private"
}
},
"questions": {
"category": {
"type": "pick",
"instructions": "Which category fits {{listing.title}} with {{listing.description}} best?",
"options": {
"bikes": "Complete bicycles and e-bikes",
"bike_parts": "Bicycle parts and accessories",
"cars": "Cars, motorbikes and scooters",
"electronics": "Computers, audio, cameras and consoles",
"phones": "Phones, tablets and smartwatches",
"home_garden": "Furniture, household items and garden",
"fashion": "Clothes, shoes and bags",
"sports": "Sports and outdoor gear other than bicycles",
"toys": "Toys and baby items",
"books_media": "Books, music, films and games",
"tickets": "Tickets and vouchers",
"other": null
},
"min_confidence": 0.5
},
"prohibited": {
"type": "check",
"instructions": "Is the item in {{listing.description}} prohibited on a general marketplace?",
"criteria": "Prohibited: weapons, drugs, counterfeit goods, stolen goods, live animals and recalled products.",
"min_confidence": 0.6
},
"condition": {
"type": "pick",
"instructions": "What condition is the item in {{listing.description}} in?",
"options": {
"new": "Unused, in its original packaging",
"as_new": "Used briefly, no visible wear",
"used": "Normal signs of use, works",
"for_parts": "Broken or only good for parts"
},
"min_confidence": 0.5
}
}
}
''')
decision = client.decide(
request["state"],
request["questions"],
)
for question_id, answer in decision.answers.items():
print(question_id, answer)// Save as dex-request.mts, then run: npx tsx dex-request.mts (Node.js 18 or newer)
import { Client, parseRequest } from "@thinqit/dex";
const client = new Client(); // reads DEX_API_KEY from the environment
// parseRequest keeps the key order of the text (JSON.parse would move labels such as "1" to the front).
const request = parseRequest(`{
"state": {
"listing": {
"title": "Gazelle city bike, 57 cm frame",
"description": "Dutch city bike with 3 gears and a front basket. Rides well, but the rear light is broken and the chain has some rust. Collect in Utrecht.",
"price_eur": 140,
"seller_type": "private"
}
},
"questions": {
"category": {
"type": "pick",
"instructions": "Which category fits {{listing.title}} with {{listing.description}} best?",
"options": {
"bikes": "Complete bicycles and e-bikes",
"bike_parts": "Bicycle parts and accessories",
"cars": "Cars, motorbikes and scooters",
"electronics": "Computers, audio, cameras and consoles",
"phones": "Phones, tablets and smartwatches",
"home_garden": "Furniture, household items and garden",
"fashion": "Clothes, shoes and bags",
"sports": "Sports and outdoor gear other than bicycles",
"toys": "Toys and baby items",
"books_media": "Books, music, films and games",
"tickets": "Tickets and vouchers",
"other": null
},
"min_confidence": 0.5
},
"prohibited": {
"type": "check",
"instructions": "Is the item in {{listing.description}} prohibited on a general marketplace?",
"criteria": "Prohibited: weapons, drugs, counterfeit goods, stolen goods, live animals and recalled products.",
"min_confidence": 0.6
},
"condition": {
"type": "pick",
"instructions": "What condition is the item in {{listing.description}} in?",
"options": {
"new": "Unused, in its original packaging",
"as_new": "Used briefly, no visible wear",
"used": "Normal signs of use, works",
"for_parts": "Broken or only good for parts"
},
"min_confidence": 0.5
}
}
}`);
const decision = await client.decide(request);
console.log(decision.answers);Expected output
{
"id": "req_01M3JE1A7KRPTYJVKBC3J9VHXQ",
"object": "decision",
"created": 1790546323,
"model": "dex-1.0.1",
"served_by": "gpu",
"calibration": "cal-20260926-1",
"answers": {
"category": {
"type": "pick",
"choice": "bikes",
"probabilities": {
"bikes": 0.8658,
"bike_parts": 0.0268,
"cars": 0.0177,
"electronics": 0.0129,
"phones": 0.0115,
"home_garden": 0.0123,
"fashion": 0.0098,
"sports": 0.0101,
"toys": 0.0076,
"books_media": 0.0071,
"tickets": 0.0084,
"other": 0.01
},
"confidence": 0.839,
"abstained": false
},
"prohibited": {
"type": "check",
"probability": 0.0802,
"confidence": 0.8397,
"abstained": false
},
"condition": {
"type": "pick",
"choice": "used",
"probabilities": {
"new": 0.0167,
"as_new": 0.0261,
"used": 0.9262,
"for_parts": 0.031
},
"confidence": 0.8952,
"abstained": false
}
},
"usage": {
"input_tokens": 289,
"state_tokens": 73,
"question_tokens": 216,
"allowance_tokens": 0,
"paid_tokens": 0,
"charge_micro_cents": 0,
"unit_price_micro_cents": 0,
"tier": "test"
}
}
Captured from the live API on 2026-09-27 with a test key: model dex-1.0.1, calibration cal-20260926-1, served_by: gpu, 289 input tokens (73 for the state, 216 for the questions). A test key is charged nothing, so tier is test and the charge is 0. On this exact version the same request always returns these answers.
| Question | Type | Answer | Confidence | min_confidence |
Abstained |
|---|---|---|---|---|---|
category |
pick | bikes (0.8658) |
0.839 | 0.5 | no |
prohibited |
check | yes with probability 0.0802 | 0.8397 | 0.6 | no |
condition |
pick | used (0.9262) |
0.8952 | 0.5 | no |
The listing is a complete bike (bikes, 0.8658), not parts (0.0268), and it is not prohibited (0.0802). Its condition is used (0.9262); for_parts gets 0.031, although the rear light is broken. No answer abstained.
Act on it
listing_id and the functions hold_for_review, publish_listing, ask_seller_for_category, set_category and set_condition stand for your own code. Here the listing is published in bikes, in used condition.
prohibited = decision.check("prohibited")
if prohibited.abstained or prohibited.probability >= 0.5:
hold_for_review(listing_id) # a person checks it before it goes live
else:
publish_listing(listing_id)
category = decision.pick("category")
if category.abstained or category.choice == "other":
ask_seller_for_category(listing_id)
else:
set_category(listing_id, category.choice)
condition = decision.pick("condition")
if not condition.abstained:
set_condition(listing_id, condition.choice)const { category, prohibited, condition } = decision.answers;
if (prohibited?.type === "check" && (prohibited.abstained || prohibited.probability >= 0.5)) {
holdForReview(listingId); // a person checks it before it goes live
} else {
publishListing(listingId);
}
if (category?.type === "pick") {
if (category.abstained || category.choice === "other") askSellerForCategory(listingId);
else setCategory(listingId, category.choice);
}
if (condition?.type === "pick" && !condition.abstained) setCondition(listingId, condition.choice);An abstained prohibited check holds the listing, the safe route for a question where a miss is expensive. As in the support recipe, the TypeScript answers have the general Answer type, so the code narrows each one on type.
Adapt it
- Deep category trees in two stages. Pick a branch first (bikes, cars, electronics), then send a second request that picks a leaf within that branch (city bike, road bike, e-bike). Each pick stays short and clear, and no pick needs more than the 255 options allowed. See Questions.
- Your prohibited list. Write each kind in
criteriain English, and add the edge cases your marketplace sees, such as replica watches counting as counterfeit. - Short descriptions for long lists. Give a description only where a label could be read two ways, and use
nullelsewhere: every description adds question tokens. - A whole catalogue. Run it with bounded concurrency and a results file: see Score a batch. On the same version a listing always gets the same category.