How to Match the Same Product Across Amazon, Walmart and eBay Listings With Jev
You search Amazon, Walmart and eBay for a Samsung T7 Portable SSD, 1TB, Titan Gray. You get about a hundred listings, but you cannot compare their prices yet. First, you need to separate that exact drive from the 2TB version, the blue version, carrying cases and used units.
Even an exact title match is not enough. A listing can name the right product while the seller information reveals that it is a used or open-box offer.
This guide builds the matching step in Python. You collect structured listing data, ask Jev three questions about each result, then use code to keep it, send it for review or drop it. The examples come from real searches run on November 8, 2026.
Why Similar Titles Are Not Enough
A common first attempt is fuzzy matching: scoring how closely two pieces of text resemble each other. You can use RapidFuzz to compare each listing title with your reference title, then keep anything above a chosen score.
The problem is that a high text-similarity score does not necessarily mean the same product.
Here are results from RapidFuzz 3.13 for the reference Samsung T7 Portable SSD 1TB Titan Gray (MU-PC1T0T/AM). The comparison uses processor=utils.default_process to standardise the text before scoring. token_set_ratio and WRatio are two scoring methods:
| Listing Title (Shortened) | Shop | token_set_ratio | WRatio | What It Really Is |
|---|---|---|---|---|
| Samsung T7 Portable SSD 1TB Titan Gray, Up-to 1,050MB/s … (MU-PC1T0T/AM) | Walmart | 100.0 | 85.5 | The product |
| Samsung MU-PC2T0T T7 2TB Portable SSD Titan Gray USB 3.2 | eBay | 87.9 | 83.5 | Wrong capacity |
| Samsung T7 1TB (MU-PC1T0T/AM) USB 3.2 Gen 2 External SSD FACTORY SEAL - GRAY | eBay | 82.8 | 78.6 | The product |
| SAMSUNG T7 Portable SSD, 1TB … MU-PC1T0H/AM, Blue | Amazon | 78.6 | 85.5 | Wrong colour |
| Samsung T7 Shield 1TB … Black, with HD-2 Drive Case | Walmart | 69.2 | 85.5 | Other model, bundled with a case |
| Paiyule Carrying Case for Samsung T9/T7/T7 Shield Portable SSD 4TB 2TB 1TB | Amazon | 69.2 | 65.8 | An accessory |
The wrong 2TB drive scores higher than a genuine match with token_set_ratio. Any cutoff that keeps that genuine eBay listing also keeps the wrong capacity. With WRatio, the blue drive and the T7 Shield bundle tie the exact Walmart match.
The words that decide whether you should keep a listing, such as “2TB”, “Blue” or “Case”, make up only a small part of its title.
Missing information is another problem. One Amazon result had the title Samsung T7 Portable SSD 1TB Titan Gray, USB 3.2 Gen 2, Up to 1,050MB/s. It scored 85.4 and was priced at $230.01 against a $274.99 list price at the time of writing. But its product page said “Sold by Amazon Resale”, Amazon’s outlet for used and open-box returns. A title comparison cannot detect information that is not in the title.
You could ask a chat model to judge each listing and return JSON, a structured data format your code can read. But you would still need to check and sometimes retry the generated response. A plain “yes” also gives you no numerical cutoff for deciding which uncertain results need review.
What Jev Adds to the Workflow
Jev returns typed answers: results in predefined forms rather than generated prose. TypeSafe describes it as its first “System One” model. As of November 2026, it accepts the information to evaluate, called the state, plus questions of types Choice, Score, Noul or Box.
You need two of those types here:
- Noul: A yes/no question that returns the probability of “yes”. You use it to ask whether a listing is the same product.
- Choice: A question with options you define. It returns a selected option, a probability for every option and a confidence value. You use it to classify condition.
Your Python code reads those results and applies your matching rules. TypeSafe’s models page lists the price as $0.042 per million input tokens, the units used to measure model input, with output tokens free.
Jev does not write text or extract a title and price from an HTML page. You must collect the listing fields before sending them.
Collect the Listings as JSON
Each shop has its own page layout and systems that block automated requests. This example uses ScrapeUnblocker’s marketplace endpoints, dedicated API routes that return search results as JSON, instead of writing a separate page parser for each shop.
Each call here uses the HTTP method POST, puts the search parameters in the URL’s query string and sends your access key in the x-scrapeunblocker-key request header.
The following code collects listing fields from the three shops. Its field names come from real responses.
import os
import requests
API = "https://api.scrapeunblocker.com"
HEADERS = {"x-scrapeunblocker-key": os.environ["SU_API_KEY"]}
def su(path, **params):
r = requests.post(API + path, params=params, headers=HEADERS, timeout=180)
r.raise_for_status()
return r.json()
def amazon_listings(keyword, max_details=10):
data = su("/marketplace/amazon-search", keyword=keyword, marketplace="amazon.com")
# The same ASIN can appear twice on one page: as a sponsored card and as an organic one.
asins = list(dict.fromkeys(card["asin"] for card in data["results"]))
listings = []
for asin in asins[:max_details]:
p = su("/marketplace/amazon-product", asin=asin, marketplace="amazon.com")
listings.append({
"shop": "amazon",
"title": p["title"],
"price": p["price"],
"currency": p["currency"],
"seller": p["seller"],
"features": (p.get("features") or [])[:3],
"url": p["url"],
})
return listings
def walmart_listings(query):
data = su("/marketplace/walmart-search", query=query)
return [{
"shop": "walmart",
"title": p["name"],
"price": p["price"],
"currency": p["currency"],
"seller": p["seller"],
"url": p["url"],
} for p in data["products"]]
def ebay_listings(keyword):
data = su("/marketplace/ebay-search", keyword=keyword,
condition="new", proxy_country="US")
if not data["exactMatches"]:
return [] # eBay's loose suggestions, not results for this keyword
return [{
"shop": "ebay",
"title": p["title"],
"price": p["price"],
"currency": p["currency"],
"condition": p["condition"],
"seller": (p.get("seller") or {}).get("username"),
"shipping": p.get("shippingCost"),
"url": p["url"],
} for p in data["results"]]
Three choices in the collection step affect matching:
- Fetch Amazon product details too. Search cards do not include the seller line that identifies the Amazon Resale offer.
/marketplace/amazon-productreturnssellerand the bullet-pointfeatures. That costs one extra request per ASIN, Amazon’s product identifier, somax_detailslimits how many detail requests you make. - Keep eBay prices in USD. eBay can convert prices to a visitor’s local currency. The request sets
proxy_country=US, and the later comparison drops prices that are not in USD.ebay_listings()also returns nothing when the response saysexactMatches: false, which marks eBay’s loose suggestions when it has no exact match. - Leave out unhelpful fields. One Walmart card named the 1TB version in its title but described the 500GB version in its snippet. This example leaves out the description to avoid sending those conflicting details.
You can add Google Shopping through /goods/google-shopping. For a shop without a dedicated endpoint, getPageSource with parsed_data=true returns product fields as JSON. See the parsed data guide for that approach, or this answer on e-commerce price scraping for collecting prices and stock levels.
Ask Three Questions About Each Listing
Install TypeSafe’s SDK, its Python library, with pip install typesafe-sdk, then set TYPESAFE_API_KEY to your API key.
The next code sends one request per listing. It puts your reference product and the candidate listing in the state, then asks whether the product matches, what condition it is in and whether it is a single item rather than a bundle.
from typesafe_sdk import Choice, Noul, TypeSafeClient
REFERENCE = {
"brand": "Samsung",
"model": "T7 Portable SSD",
"capacity": "1TB",
"colour": "Titan Gray",
"part_number": "MU-PC1T0T (any region suffix, e.g. /AM or /WW)",
"different_models": ["T7 Shield", "T7 Touch", "T9"],
}
QUESTIONS = {
"same_product": Noul(
instructions="Is this `listing` the same product as the `reference`: same model, same size or capacity, same colour?",
),
"condition": Choice(
instructions="In what condition is the item in `listing` sold?",
criteria={
"new": "A new, unopened unit of the product itself",
"refurbished": "Refurbished, renewed or restored",
"used": "Used, pre-owned, open box or returned, including offers sold by Amazon Resale",
"accessory_or_part": "A case, cable, adapter or spare part, not the product itself",
},
),
"is_bundle": Noul(
instructions="Is this `listing` a bundle or multi-pack rather than the single item?",
),
}
def ask_jev(client, listing):
fields = {k: v for k, v in listing.items() if k != "url"}
return client.system_one(
state={"reference": REFERENCE, "listing": fields},
questions=QUESTIONS,
)
The request is structured this way for a few reasons:
- Name the information you send. The state has separate
listingandreferenceparts. TypeSafe recommends this structured object over a plain string, and the questions refer to those names. - Include what you know about the product. Jev is not trained on your catalogue. The
different_modelsfield tells it about lookalikes, while the Amazon Resale hint in theusedoption helps it interpret that seller information. - Keep separate facts in separate questions. “The right drive, new, single unit” contains three checks. TypeSafe advises asking one yes/no question per Noul and combining the results in code. The questions run in parallel within the request.
- Include the price. A $9.99 listing presented as an SSD is a useful clue that it may be an accessory rather than the drive.
Decide What to Keep, Review or Drop
The model’s answers are inputs to your decision, not the final decision itself. The following code applies probability cutoffs and assigns each listing to keep, review or drop.
SAME_YES = 0.8 # starting points: tune them on listings you labelled by hand
SAME_MAYBE = 0.5
SURE = 0.8
def decide(response):
same = response.nouls["same_product"].noul
bundle = response.nouls["is_bundle"].noul
condition = response.choices["condition"]
p_condition = condition.probabilities[condition.choice]
if same < SAME_MAYBE:
return "drop" # another product: ignore the other two answers
if same < SAME_YES or p_condition < SURE or 0.3 <= bundle <= 0.7:
return "review" # unsure about something that changes the price
if condition.choice != "new" or bundle > 0.7:
return "drop" # right product, but used, refurbished or a multi-pack
return "keep"
def compare(query):
listings = amazon_listings(query) + walmart_listings(query) + ebay_listings(query)
listings = [x for x in listings if x["price"] is not None and x["currency"] == "USD"]
keep, review = [], []
with TypeSafeClient(model="jev-latest") as client: # Reads TYPESAFE_API_KEY.
for listing in listings:
verdict = decide(ask_jev(client, listing))
if verdict == "keep":
keep.append(listing)
elif verdict == "review":
review.append(listing)
for x in keep:
x["total"] = x["price"] + (x.get("shipping") or 0)
keep.sort(key=lambda x: x["total"])
for x in keep:
print(f'{x["total"]:>8.2f} {x["shop"]:<8} {x["title"][:70]}')
print(f"\n{len(review)} listings to check by hand:")
for x in review:
print(f' {x["shop"]:<8} {x["url"]}')
compare("samsung t7 portable ssd 1tb")
decide() checks same_product first. That order matters because the condition Choice always selects one of its four options, even when the listing is for a different drive. A condition answer is relevant only after you establish that the product matches. TypeSafe calls this the companion Noul pattern.
For the Choice answer, the code checks probabilities[choice], the probability assigned to the selected option, rather than confidence. TypeSafe’s routing by probability documentation explains that confidence for a fixed-option Choice is calculated from the top probability. Adding a confidence cutoff therefore does not add another independent check.
If you want to process several listings at once rather than wait for each request to finish, the SDK includes AsyncTypeSafeClient.
Tune the Cutoffs With Listings You Have Checked
The 0.8 and 0.5 cutoffs in the code are starting points. A probability is not a guarantee that one answer is correct. TypeSafe notes that calibration, how well predicted probabilities line up with observed results, is measured across groups of predictions.
Choose your cutoffs using listings from your own searches:
- Label 50 to 100 listings by hand. Record whether each is the same product, its condition and whether it is a single item or bundle. Include wrong capacities, colours, cases, Shield models and T9 lookalikes.
- Run
ask_jev()on those listings. Save all three answers beside your labels. - Compare the scores with your labels. Set
SAME_YESabove the non-matches you cannot afford to keep. Use the band below that cutoff for manual review.
A false match that changes prices automatically can be more costly than one in a weekly report someone checks. Raise SAME_YES for the former. TypeSafe also warns against tuning thresholds on only a handful of examples, which is why this starts with 50 listings rather than 5.
Once you have tuned the rules, use a fixed model version. As of November 2026, jev-latest points to jev-1.14.0, but it changes when a new release ships. Your answers could therefore change without any code change. Use the versioned model ID and record response.model with each result.
Where This Approach Falls Short
Exact Identifiers Should Come First
If both records carry a UPC (the barcode number) or a full manufacturer part number, compare those in code before using a model.
Samsung’s part numbers distinguish variants: MU-PC1T0T identifies the 1TB Titan Gray version, while MU-PC1T0H identifies the 1TB blue version. Send listings without a usable identifier to Jev. The Walmart product endpoint returns a upc, but the search cards from all three shops do not.
You Still Need Reliable Listing Data
Jev does not fetch pages. Collecting data from shops that block scrapers remains a separate task, and at volume it is usually the step that fails first.
Your Data Goes to an External Service
The listing fields and your reference product are sent to TypeSafe’s API. Check that this is allowed under your data-handling rules before using the workflow.
FAQ
Can I match products across shops without an AI model?
Yes. If both listings share a UPC, EAN or manufacturer part number, standardise its formatting and match on that identifier. Title similarity alone cannot reliably separate capacities, colours and accessories.
Why not one Choice with “same”, “different” and “accessory” as options?
You still need separate checks for condition and quantity. The right product can be used or sold in a 2-pack. Separate questions let your code apply your own rules to those facts instead of relying on one combined label.
Can I check many listings in one Jev request?
Yes. TypeSafe’s Noul documentation shows one record compared with multiple candidates in one request, using one Noul per candidate. The request must fit within Jev 1.14’s default limit of 64k tokens. One listing per request is easier to tune because each set of answers belongs to one listing.
Does this work for shops other than Amazon, Walmart and eBay?
Yes. Jev evaluates the fields you supply, not the shop itself. Collect another shop’s fields with getPageSource and parsed_data=true, map them into the same listing structure, and keep the rest of the workflow.
Start With One Product
Choose one product you track. Fill in its reference object, including the lookalikes you already know about, and run compare() on one search. Check the listings sent for review and use those labels as your first tuning set.
The collection step here uses ScrapeUnblocker’s marketplace endpoints. The Amazon plugin docs list the available parameters and fields, with Walmart and eBay documentation alongside them. You can get an API key at app.scrapeunblocker.com.
Try ScrapeUnblocker free
95%+ success rate · from 0.55€ per 1,000 calls (VAT may apply) · 500 free requests on signup.