The Kamion Pricing Engine is a photo-in, price-range-out system for Class 7/8 trucks. A seller uploads listing photos; a vision-language model extracts truck type, brand, and condition; a statistical model turns those features into a USD asking range. Kamion is a marketplace that has to move trucks from sellers to buyers. A listing priced too high sits unsold. A listing priced too low leaves money on the table and trains the next seller to distrust the platform. The engine exists to replace guesswork with a range that is grounded in real TruckPaper asking prices, so more trucks actually clear.
This repo is the full pipeline behind that range: a stratified TruckPaper image scrape, an audit that drops unusable listings, a supervised Class 7/8 filter, RLHF-style instruction loops for four VLM tasks, and a type + brand + condition price model.
Kamion matches sellers who have a used truck with buyers who will actually pay for it. Pricing is the bottleneck. Sellers often copy a number from a neighboring listing, or ask for what they owe on the truck, not what the market will clear. Buyers bounce when the ask is outside a believable band. Kamion needs a pricing algorithm that can look at the same photos a buyer sees and return a range that is specific to what the truck is (day cab, sleeper, dump), who built it (Freightliner, Kenworth, Peterbilt, International, Mack), and what shape it is in (frame rust, front-end damage, tread, cab/aero).
That is the only job of this engine: turn photos into a defensible [low, high] USD range so more trucks sell.
TruckPaper listings
(Class 7/8 + Class 2–6)
│
▼
┌─────────────────────────────────────────────────────────┐
│ Dataset │
│ scrape → audit (price / USD / SHA-256 dedup) │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Supervised Class 7/8 filter │
│ gold labels from scrape folders │
│ VLM truck_type ∈ {day-cab, dump, sleeper, none} │
└─────────────────────────────────────────────────────────┘
│ valid Class 7/8 only
▼
┌─────────────────────────────────────────────────────────┐
│ RLHF VLM tasks (one master schema, four stages) │
│ 1. truck type / validity │
│ 2. primary subject (must be front or side) │
│ 3. brand (read off a badge, never guess) │
│ 4. condition (4 categories → penalty %) │
└─────────────────────────────────────────────────────────┘
│ type + brand + category penalties
▼
┌─────────────────────────────────────────────────────────┐
│ Price range model │
│ μ(type, brand) + condition effect → [low, high] │
└─────────────────────────────────────────────────────────┘
│
▼
USD asking range for the listing
Market comps come from TruckPaper listing photos, not from synthetic data. The scraper in image_scraper/scripts/truckpaper_image_scraper.py impersonates Chrome TLS with curl_cffi so Cloudflare does not serve a JS challenge, then walks allowlisted category searches.
Every search is scoped to one category and one GVWR class. Positives are Class 7/8 (GVWR 26,001 lb and up). Negatives are Class 2–6 and exist so the filter has hard counterexamples.
| Split | Categories | Class mix | Image target |
|---|---|---|---|
| Positive (Class 7–8) | sleeper, day cab, dump | 15% Class 7, 85% Class 8 | 10,000 full / 200 test |
| Negative (Class 2–6) | 1-ton and 3/4-ton pickups, cargo vans, step vans, cutaway cubes, moving and cargo box trucks, stake, flatbed, service/utility | 45 / 22 / 18 / 10 / 5% across Classes 6→2 | 5,000 full / 100 test |
Inside each category the scrape is stratified again:
- Year buckets: 1900–2012, 2013–2017, 2018–2021, 2022–present
- Price terciles: 45% cheap / 30% mid / 25% high / 10% no listed price, so the model is not trained only on premium trucks
- Brand cap: no brand may take more than 30% of a stratum
- Listing rules: at least 5 photos, real asking price (auctions and "Call for Price" are skipped), trucks only (no trailers, bodies-only, or dismantled units)
Listings are named by category, not by TruckPaper's numeric ID (sleeper_truck_07/sleeper_truck_07_image_03.jpg), and a manifest (truckpaper_scraped_listings.json) stores id, brand, price, currency, and image paths. Re-runs resume from .state/ so a --full job continues a test scrape instead of starting over.
The priced Class 7/8 population used by the model is 358 listings after cleanup:
| Type | n | Typical log-price center |
|---|---|---|
| Dump | 142 | ~$83k |
| Day cab | 130 | ~$31k |
| Sleeper | 86 | ~$36k |
| Brand cell | n |
|---|---|
| Freightliner | 112 |
| International | 61 |
| Kenworth | 58 |
| Peterbilt | 52 |
| Mack | 42 |
| Other | 33 |
image_scraper/scripts/audit_truckpaper_image_dataset.py is a destructive cleanup that defaults to a dry run. Three checks run in order, each dropping a listing or a single image:
- Bad price —
priceis null (TruckPaper showed "Call for Price" or a non-positive number). A price model cannot train on a missing target. - Non-USD currency — anything other than exactly
USDis dropped so the price column is comparable. - Duplicate images — SHA-256 of file bytes. A hash that repeats inside one listing, or across listings in the same top-level split (positive or negative), is treated as a placeholder or a copy. The first occurrence is kept. A hash shared between the two splits is left alone.
After dedup, surviving images are renamed to close gaps (_image_01, _image_02, …) and the manifest is rewritten. A listing left with zero images is dropped. Apply with --apply.
The vision stack is not one prompt. It is four tasks that were written as separate JSON schemas, scored against labels, rewritten when they failed, and finally fused into vlm_instructions/truck_feature_extraction_master_instructions.json.
The scrape folders are the gold labels: every image under positive (class 7 - 8)/ is a valid Class 7/8 truck (or a part of one); every image under negative (class 2 - 6)/ is not. That gives a supervised classification problem before any human has to sit down and tag photos.
input_image_filter_test/ builds a stratified blind eval set, resizes the long edge to 1568 px, and hides folder names behind opaque IDs (img_001.jpg). A Sonnet agent fills truck_type from pixels only. Validity is implied:
truck_type |
Meaning |
|---|---|
day-cab-truck |
Valid Class 7/8 day cab or heavy conventional cab |
dump-truck |
Valid Class 7/8 dump / hopper / grain body |
sleeper-truck |
Valid Class 7/8 sleeper box or sleeper interior |
none |
Invalid — Class 2–6, non-truck, or a part that only those lighter vehicles have |
evaluate_predictions.py scores the agent against the gold folders (schema validity, Class 7/8 accuracy, exact type accuracy, confusion matrix). Those errors are the supervised signal. Instructions get rewritten — close-ups of duals and dump tailgates must count as valid, Super Duty chassis-cabs and enclosed box bodies must not — and the eval is re-run. That is the first filter every later task sits behind.
Once the filter is stable, three more schemas are refined the same way: draft a strict JSON contract, have the VLM label a blind batch, compare the output to human judgment, change the rules that caused the miss, repeat.
Primary subject. Center-weighted composition, not raw pixel area. Allowed labels: front, side, back, engine, container, interior, dashboard, tires, unusable. Background trucks in a yard are normal and do not make an image unusable. The pricing path only accepts front or side — those are the shots where type, badge, and condition are actually visible.
Brand. Read a logo, hood ornament, badge, or wordmark off the cab door, grille, roof cap, or steering wheel. Cross-check the string against US truck OEMs (Freightliner, Peterbilt, Kenworth, Mack, International/Navistar, Volvo, Western Star, plus vocational builders). Never guess from grille shape or paint. If nothing is readable, set needs_user_input and ask the seller — do not invent a brand.
Condition. Four categories, scored 1–5 from visible evidence only, each with a penalty percent:
| Category | Weight | What it inspects | Max penalty |
|---|---|---|---|
| Chassis and frame | 35% | Frame rails, leaf springs, hangers, U-bolts | 30% |
| Front end / hood / engine bay | 25% | Grille, bumper, hood, visible engine bay | 10% |
| Tires / wheels / suspension | 20% | Tread, rims, visible wear | 8% |
| Cab / sleeper / aero | 20% | Interior (only if shown) and fairings | 5% |
A part that is out of frame is null and dropped from the weighted average — the model does not invent rust it cannot see.
The four tasks run in a fixed order inside a single Sonnet call (reasoning, then output). Brand and condition are skipped unless truck_type ≠ none and primary_subject ∈ {front, side}. That is the definition of a priceable image. Everything else is rejected with a short user message asking for a better photo.
Human corrections on failed batches are written back into the schemas. That is the RLHF loop: the reward is agreement with gold labels (filter) or with the written rubric (subject, brand, condition), and the policy that gets updated is the instruction JSON, not a weight file.
The VLM does not predict price. It predicts features. pricing/fit_price_range.py fits a log-price model on the 358 priced Class 7/8 listings, then uses the VLM labels to estimate how condition moves that price.
For every listing with a USD ask and a brand, take log(price). Shrink each cell toward its parent so a rare dump × Mack row does not overfit:
μ_cell = (n / (n + k)) · raw_μ + (1 − n / (n + k)) · parent_μ
k (shrinkage) is grid-searched. The type table is the parent of each type×brand cell. Brands outside Freightliner, International, Kenworth, Peterbilt, and Mack collapse to OTHER.
μ = brand_blend · μ_(type, brand) + (1 − brand_blend) · μ_type
brand_blend = 1 in the saved model: when a cell exists, use it; otherwise fall back to the type mean.
The four VLM penalties are combined with the rubric weights (chassis 0.35, front 0.25, tires 0.20, cab 0.20). Ordinary least squares then fits β on the labeled set:
log(price) − μ = intercept + β · (weighted_penalty − penalty_bar)
β is negative: a dirtier truck is cheaper than its type×brand peers. condition_scale stretches or shrinks that effect and is tuned with the other weights.
center = exp( μ + condition_scale · β · (weighted_penalty − penalty_bar) )
low = center · (1 − error_bound)
high = center · (1 + error_bound)
error_bound is the leave-one-out median absolute percentage error on the labeled trucks (about ±36% in the current fit). The range is the number Kamion can show a seller: not a single lucky point, a band the market has actually cleared.
Weights (shrink_k, brand_blend, condition_scale, category mix) are chosen by grid search to minimize that LOO median APE. The winner is written to pricing/price_range_model.json.
pricing/predict.py accepts a photo, a filled VLM JSON, or explicit --truck-type / --brand / --penalty flags. Photos are resized and run through the master schema. Non-priceable images exit with a user message. Missing brand exits and asks the seller. Otherwise the model returns center, low, high, and the features that produced them.
python pricing/predict.py --image path/to/truck.jpg
python pricing/predict.py --truck-type day_cab --brand FREIGHTLINER --penalty 4.0
- Python 3.11+ — python.org
- Cursor (or any Claude Sonnet workspace) for in-chat VLM labeling — no separate API key is required for the batch workflow
git clone https://github.com/YOUR_USER/54hackathon.git
cd 54hackathon
python -m venv .venvmacOS / Linux: source .venv/bin/activate
Windows: .venv\Scripts\activate
pip install -r image_scraper/requirements.txtpython image_scraper/scripts/truckpaper_image_scraper.py # 200 + 100 image test
python image_scraper/scripts/truckpaper_image_scraper.py --full # 10k + 5k
python image_scraper/scripts/truckpaper_image_scraper.py plan # print strata and quotaspython image_scraper/scripts/audit_truckpaper_image_dataset.py # dry run
python image_scraper/scripts/audit_truckpaper_image_dataset.py --applypython input_image_filter_test/sample_eval_set.py
python input_image_filter_test/stage_blind_images.py
# fill data/predictions.json from the filter schema, then:
python input_image_filter_test/evaluate_predictions.pypython pricing/build_sample.py
python pricing/label_sample.py
# fill pricing/batches/batch_XX.json with the master schema
python pricing/merge_batch_labels.py
python pricing/fit_price_range.pypython pricing/predict.py --image path/to/truck.jpgMIT