WIP: epitaxy pre-switch from baalxp

This commit is contained in:
alexpolo1
2026-09-05 09:42:53 +02:00
parent d0d2a54999
commit 165e4385d4
7 changed files with 569 additions and 0 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

+114
View File
@@ -0,0 +1,114 @@
# Convert the stash sale list into the CSV that tools/traderie_lister.py expects.
#
# python scripts/make_traderie_csv.py # everything sellable
# python scripts/make_traderie_csv.py --runes # runes only
#
# Writes traderie_items.csv, then:
# python tools/traderie_lister.py --csv traderie_items.csv --dry-run
import csv
import os
import re
import sys
_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
os.chdir(_root)
SRC = "stash_sale_list.csv"
OUT = "traderie_items.csv"
COLUMNS = ["name", "qty", "base_tier", "base_item", "ring_variant",
"amulet_variant", "ethereal", "pricing", "price_amount",
"price_currency"]
# Runewords need a "Base Item" field. "No base" is the lister's own escape
# hatch for when the socketed base does not matter to the listing.
RUNEWORDS = {"spirit", "insight", "stealth", "lore", "smoke", "rhyme",
"ancients pledge", "leaf", "malice", "steel", "zephyr", "king's grace",
"edge", "harmony", "white", "peace", "myth", "black", "honor"}
# Not tradeable / not worth a listing. Potions are consumables; sub-perfect gems
# were already dropped upstream but are re-checked here so this script is safe to
# run against any sale list.
SKIP_WORDS = ("rejuvenation potion", "scroll of", "tome of", "key of",
"chipped", "flawed", "flawless")
def title_case(name: str) -> str:
"""Stash tooltips are ALL CAPS; Traderie searches title case.
Apostrophes matter: "TAL RASHA'S" must become "Tal Rasha's", not
"Tal Rasha'S", or the dropdown search misses.
"""
out = []
for word in name.lower().split():
if "'" in word:
head, _, tail = word.partition("'")
out.append(head.capitalize() + "'" + tail)
else:
out.append(word.capitalize())
return " ".join(out)
def main():
runes_only = "--runes" in sys.argv
with open(SRC, encoding="utf-8") as f:
rows = list(csv.DictReader(f))
seen: dict[str, dict] = {}
skipped = 0
for r in rows:
raw = (r["name"] or "").strip()
low = raw.lower()
if not raw or any(w in low for w in SKIP_WORDS):
skipped += 1
continue
if runes_only and not low.endswith("rune"):
continue
name = title_case(raw)
qty = r.get("qty") or "1"
try:
qty = int(qty)
except ValueError:
qty = 1
# The same item can sit in several cells (2x TARNHELM, 4x SMALL CHARM OF
# INERTIA). Traderie wants one listing with a quantity, not N listings.
if name in seen:
seen[name]["qty"] += qty
continue
base_item = "No base" if name.lower() in RUNEWORDS else ""
seen[name] = {
"name": name,
"qty": qty,
"base_tier": "",
"base_item": base_item,
"ring_variant": "",
"amulet_variant": "",
"ethereal": "",
# You said you would set prices yourself — ask_for_offers means the
# listing goes up without committing to a number.
"pricing": "ask_for_offers",
"price_amount": "",
"price_currency": "",
}
out = sorted(seen.values(), key=lambda r: (not r["name"].endswith("Rune"), r["name"]))
with open(OUT, "w", newline="", encoding="utf-8") as f:
w = csv.DictWriter(f, fieldnames=COLUMNS)
w.writeheader()
w.writerows(out)
print(f"wrote {OUT}: {len(out)} listings ({skipped} skipped as untradeable)")
runes = [r for r in out if r["name"].endswith("Rune")]
print(f" runes: {len(runes)} other: {len(out) - len(runes)}")
needs_base = [r["name"] for r in out if r["base_item"]]
if needs_base:
print(f" runewords given base_item='No base': {', '.join(needs_base)}")
print("\nnext:")
print(f" python tools/traderie_lister.py --csv {OUT} --dry-run")
if __name__ == "__main__":
main()
Binary file not shown.
Binary file not shown.
+380
View File
@@ -0,0 +1,380 @@
#!/usr/bin/env python3
"""
traderie_lister.py
Automates creating "sell" listings on Traderie (https://traderie.com) for
Diablo II: Resurrected, based on the workflow used to bulk-list ~20 items
for account "alexpolo" in September 2026.
HOW IT WORKS
------------
Traderie's "Add Listing" form is a React app built on react-select style
dropdowns. It does NOT have simple <select> elements, so this script drives
it like a human would: click the combobox, wait for the option list to
render, click the matching option by visible text.
The set of fields shown on the form depends on the item you pick:
- Runes / plain items: Ladder, Platform, Mode, Game version
- Ethereal-capable uniques: + "Ethereal" checkbox
- Charms / belts / boots / etc: + "Base Tier" (Normal/Exceptional/Elite)
- Rings: + "Ring Variant"
- Amulets w/ multiple looks: + "Amulet Variant"
- Runewords: + "Base Item" (weapon/armor the rune-
word is socketed into; select "No base"
if you don't know / don't care)
- Jewels (e.g. Rainbow Facet): + a cosmetic gem-color dropdown (any
value is fine, it's just the icon
color shown in the listing)
This script fills in whichever of those fields are present and skips
whichever aren't, using best-effort text matching.
LOGIN
-----
Traderie authenticates via Discord OAuth. This script does NOT attempt to
log in for you. Run once with `--setup-login` to open a real browser
window, log in manually, and save the session (cookies + localStorage) to
`auth_state.json`. Every subsequent run reuses that file headlessly.
USAGE
-----
pip install playwright
playwright install chromium
# one-time login capture
python traderie_lister.py --setup-login
# dry run (prints what it would do, doesn't submit)
python traderie_lister.py --csv items.csv --dry-run
# for real
python traderie_lister.py --csv items.csv
CSV FORMAT
----------
name,qty,base_tier,base_item,ring_variant,amulet_variant,ethereal,pricing,price_amount,price_currency
Ral Rune,9,,,,,,ask_for_offers,,
Waterwalk,1,Normal,,,,,ask_for_offers,,
Spirit,1,,No base,,,,ask_for_offers,,
Insight,1,Elite,No base,,,,ask_for_offers,,
Dwarf Star,1,,,Orange,,,ask_for_offers,,
Tal Rasha's Adjudication,1,,,,Sun,,ask_for_offers,,
Nagelring,3,,,,,,fixed,5,Pul Rune
Notes on columns:
name Exact (or close) item name as it appears in Traderie's
search. The script picks the first dropdown match, or an
exact-text match if one exists among the results.
qty Stack amount. Defaults to 1 if blank.
base_tier Normal / Exceptional / Elite (only for items that ask).
base_item For runewords: the weapon/armor base, or "No base".
ring_variant Big Blue / Orange / Crown / Coral / Small Blue, etc.
amulet_variant Whatever the dropdown offers (e.g. "Sun", "Dot", "Penta").
ethereal "yes"/"true" to check the Ethereal box.
pricing "ask_for_offers", "free", or "fixed".
price_amount Only used when pricing == fixed.
price_currency Only used when pricing == fixed (searched in the "Runes"
or "Items You're Looking For" combobox depending on type).
ACCOUNT-WIDE SETTINGS
----------------------
Ladder, Platform, and Mode are the same for every listing on a given
account/session, so they're passed once as CLI flags rather than per-row:
--ladder ladder|non_ladder (default: ladder)
--platform pc|switch|playstation|xbox (default: pc)
--mode softcore|hardcore (default: softcore)
--game-version classic|lod|row (default: row = "reign of the warlock")
"""
import argparse
import csv
import json
import sys
import time
from pathlib import Path
from playwright.sync_api import sync_playwright, TimeoutError as PWTimeout
ADD_LISTING_URL = "https://traderie.com/diablo2resurrected/listings/create"
AUTH_STATE_FILE = "auth_state.json"
GAME_VERSION_LABELS = {
"classic": "classic (base game)",
"lod": "lord of destruction",
"row": "reign of the warlock",
}
PLATFORM_LABELS = {
"pc": "PC",
"switch": "switch",
"playstation": "playstation",
"xbox": "xbox",
}
MODE_LABELS = {
"softcore": "softcore",
"hardcore": "hardcore",
}
LADDER_LABELS = {
"ladder": "Ladder",
"non_ladder": "Non Ladder",
}
def log(msg):
print(f"[traderie] {msg}", flush=True)
def setup_login():
"""Open a real browser, let the user log in via Discord, save the
session so future headless runs are already authenticated."""
with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
context = browser.new_context()
page = context.new_page()
page.goto("https://traderie.com/diablo2resurrected")
log("Log in through the browser window that just opened (Discord OAuth).")
log("Once you're logged in and can see your avatar in the top-right, "
"come back here and press Enter.")
input()
context.storage_state(path=AUTH_STATE_FILE)
log(f"Saved session to {AUTH_STATE_FILE}. You can close the browser.")
browser.close()
def select_react_dropdown(page, label_text, option_text, exact=False, timeout=8000):
"""Click a react-select style combobox identified by its adjacent
<label>/heading text, then click the option whose text matches
option_text (case-insensitive substring match unless exact=True)."""
# Find the "Select..." control that sits right after the field's label.
# Traderie doesn't use <label for=>, so we locate by nearby text.
field = page.locator(
f"xpath=//*[self::label or self::div or self::span][contains(normalize-space(.), '{label_text}')]"
f"/following::div[contains(@class,'select') or .//input][1]"
).first
field.click(timeout=timeout)
page.wait_for_timeout(200)
if exact:
option = page.get_by_text(option_text, exact=True).last
else:
option = page.locator(f"text=/{option_text}/i").last
option.click(timeout=timeout)
page.wait_for_timeout(150)
def click_option_in_open_dropdown(page, option_text, exact=False, timeout=8000):
"""Assumes a dropdown is already open; clicks the matching option."""
if exact:
option = page.get_by_text(option_text, exact=True).last
else:
option = page.locator(f"text=/{option_text}/i").last
option.click(timeout=timeout)
page.wait_for_timeout(150)
def set_amount(page, qty):
amt = page.locator("text=Amount").locator(
"xpath=following::input[1]"
).first
amt.click(click_count=3)
amt.fill(str(qty))
def search_and_select_item(page, item_name):
"""Types into the 'Search Items...' combobox and clicks the best
matching result."""
search = page.get_by_placeholder("Search Items...").first
search.click()
search.fill(item_name)
page.wait_for_timeout(1200)
# Prefer an exact (case-insensitive) text match among results; fall back
# to the first result rendered.
results = page.locator("div:has-text('" + item_name + "')").all()
exact = page.get_by_text(item_name, exact=True)
try:
exact.first.click(timeout=1500)
except PWTimeout:
# fall back: click the first row-like result under the input
page.locator("xpath=//input[@placeholder='Search Items...']"
"/ancestor::div[1]/following::div[1]").first.click()
page.wait_for_timeout(300)
def try_set_dropdown_by_label(page, label, value, exact=False):
"""Best-effort: find a 'Select...' box under a heading matching
`label` and pick `value`. Silently no-ops if the field isn't present
on this item's form (e.g. Base Tier on a plain rune)."""
try:
heading = page.locator(f"text=/^{label}$/i").first
heading.wait_for(state="visible", timeout=1500)
except PWTimeout:
return False
box = heading.locator("xpath=following::div[contains(@class,'select')][1]").first
box.click(timeout=3000)
page.wait_for_timeout(200)
click_option_in_open_dropdown(page, value, exact=exact)
return True
def fill_listing(page, row, ladder, platform, mode, game_version, dry_run=False):
name = row["name"].strip()
qty = row.get("qty", "").strip() or "1"
base_tier = row.get("base_tier", "").strip()
base_item = row.get("base_item", "").strip()
ring_variant = row.get("ring_variant", "").strip()
amulet_variant = row.get("amulet_variant", "").strip()
ethereal = row.get("ethereal", "").strip().lower() in ("yes", "true", "1")
pricing = (row.get("pricing", "").strip() or "ask_for_offers").lower()
price_amount = row.get("price_amount", "").strip()
price_currency = row.get("price_currency", "").strip()
log(f"--- Listing: {name} x{qty} ---")
if dry_run:
log(f" (dry run) would select item '{name}', qty={qty}, "
f"base_tier={base_tier or '-'}, base_item={base_item or '-'}, "
f"ring_variant={ring_variant or '-'}, amulet_variant={amulet_variant or '-'}, "
f"ethereal={ethereal}, pricing={pricing}, "
f"price={price_amount or '-'} {price_currency or ''}")
return
page.goto(ADD_LISTING_URL)
page.wait_for_selector("text=Item you're trading", timeout=15000)
search_and_select_item(page, name)
page.mouse.wheel(0, 400)
page.wait_for_timeout(300)
set_amount(page, qty)
if ethereal:
try:
page.get_by_text("Ethereal", exact=True).first.click(timeout=1500)
except PWTimeout:
pass
# Account-wide fields
try_set_dropdown_by_label(page, "Ladder", LADDER_LABELS[ladder], exact=True)
try_set_dropdown_by_label(page, "Platform", PLATFORM_LABELS[platform], exact=True)
try_set_dropdown_by_label(page, "Mode", MODE_LABELS[mode], exact=True)
# Item-specific fields (no-ops if not present on this item's form)
if base_item:
try_set_dropdown_by_label(page, "Base Item", base_item)
if ring_variant:
try_set_dropdown_by_label(page, "Ring Variant", ring_variant, exact=True)
if amulet_variant:
try_set_dropdown_by_label(page, "Amulet Variant", amulet_variant, exact=True)
if base_tier:
try_set_dropdown_by_label(page, "Base Tier", base_tier, exact=True)
try_set_dropdown_by_label(page, "Game version", GAME_VERSION_LABELS[game_version])
# Pricing
page.mouse.wheel(0, 400)
page.wait_for_timeout(300)
if pricing == "ask_for_offers":
page.get_by_text("Ask for Offers", exact=True).first.click()
elif pricing == "free":
page.get_by_text("Free", exact=True).first.click()
elif pricing == "fixed":
page.get_by_text("Accept Only Listing Price", exact=True).first.click()
# Runes or general items box for the price currency/item
price_box = page.get_by_placeholder("Search Runes...")
try:
price_box.click(timeout=1000)
except PWTimeout:
price_box = page.get_by_placeholder("Search Items...").last
price_box.click()
price_box.fill(price_currency)
page.wait_for_timeout(1000)
page.get_by_text(price_currency, exact=False).first.click()
qty_box = page.locator(
"xpath=//input[@placeholder='Search Runes...' or @placeholder='Search Items...']"
"/following::input[1]"
).last
if price_amount:
qty_box.click(click_count=3)
qty_box.fill(price_amount)
else:
log(f" WARNING: unknown pricing mode '{pricing}', skipping pricing step")
page.mouse.wheel(0, 600)
page.wait_for_timeout(400)
submit = page.get_by_role("button", name="Add Listing").first
submit.click()
try:
page.wait_for_selector("text=Your listing has been submitted", timeout=10000)
log(" Submitted OK.")
except PWTimeout:
log(" WARNING: did not see the submission confirmation message. "
"Check the page manually.")
# Reset for next item
try:
page.get_by_role("button", name="OK").first.click(timeout=2000)
except PWTimeout:
pass
def main():
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--setup-login", action="store_true",
help="Open a browser to log in and save the session.")
ap.add_argument("--csv", type=str, help="Path to the items CSV.")
ap.add_argument("--dry-run", action="store_true",
help="Print what would happen without submitting anything.")
ap.add_argument("--headless", action="store_true", default=False,
help="Run browser headless (default: headed, so you can watch).")
ap.add_argument("--ladder", choices=list(LADDER_LABELS), default="ladder")
ap.add_argument("--platform", choices=list(PLATFORM_LABELS), default="pc")
ap.add_argument("--mode", choices=list(MODE_LABELS), default="softcore")
ap.add_argument("--game-version", choices=list(GAME_VERSION_LABELS), default="row")
args = ap.parse_args()
if args.setup_login:
setup_login()
return
if not args.csv:
ap.error("--csv is required unless using --setup-login")
rows = list(csv.DictReader(open(args.csv, newline="", encoding="utf-8")))
if not rows:
log("CSV has no rows, nothing to do.")
return
if not args.dry_run and not Path(AUTH_STATE_FILE).exists():
log(f"No {AUTH_STATE_FILE} found. Run with --setup-login first, "
f"or use --dry-run to test without logging in.")
sys.exit(1)
with sync_playwright() as p:
browser = p.chromium.launch(headless=args.headless)
if args.dry_run:
context = browser.new_context()
else:
context = browser.new_context(storage_state=AUTH_STATE_FILE)
page = context.new_page()
for i, row in enumerate(rows, 1):
log(f"[{i}/{len(rows)}]")
try:
fill_listing(page, row, args.ladder, args.platform,
args.mode, args.game_version, dry_run=args.dry_run)
except Exception as e:
log(f" ERROR on '{row.get('name')}': {e!r} -- continuing with next item")
time.sleep(0.5)
browser.close()
log("Done.")
if __name__ == "__main__":
main()
+75
View File
@@ -0,0 +1,75 @@
name,qty,base_tier,base_item,ring_variant,amulet_variant,ethereal,pricing,price_amount,price_currency
Amn Rune,10,,,,,,ask_for_offers,,
Cham Rune,1,,,,,,ask_for_offers,,
Dol Rune,9,,,,,,ask_for_offers,,
El Rune,2,,,,,,ask_for_offers,,
Eld Rune,2,,,,,,ask_for_offers,,
Eth Rune,9,,,,,,ask_for_offers,,
Fal Rune,1,,,,,,ask_for_offers,,
Hel Rune,2,,,,,,ask_for_offers,,
Io Rune,2,,,,,,ask_for_offers,,
Ith Rune,1,,,,,,ask_for_offers,,
Ko Rune,1,,,,,,ask_for_offers,,
Lem Rune,2,,,,,,ask_for_offers,,
Lum Rune,1,,,,,,ask_for_offers,,
Mal Rune,1,,,,,,ask_for_offers,,
Nef Rune,4,,,,,,ask_for_offers,,
Ohm Rune,1,,,,,,ask_for_offers,,
Ort Rune,23,,,,,,ask_for_offers,,
Ral Rune,9,,,,,,ask_for_offers,,
Sol Rune,7,,,,,,ask_for_offers,,
Tal Rune,17,,,,,,ask_for_offers,,
Thul Rune,6,,,,,,ask_for_offers,,
Tir Rune,10,,,,,,ask_for_offers,,
Um Rune,4,,,,,,ask_for_offers,,
Amber Grand Charm Of Sustenance,1,,,,,,ask_for_offers,,
Bitter Brogues,1,,,,,,ask_for_offers,,
Burning Grand Charm,1,,,,,,ask_for_offers,,
Coral Small Charm Of Incineration,1,,,,,,ask_for_offers,,
Death Hold,1,,,,,,ask_for_offers,,
Death Mask,1,,,,,,ask_for_offers,,
Deep Worldstone Shard,1,,,,,,ask_for_offers,,
Dwarf Star,1,,,,,,ask_for_offers,,
Eagle Torc,1,,,,,,ask_for_offers,,
Eastern Worldstone Shard,1,,,,,,ask_for_offers,,
Entrapping Grand Charm,1,,,,,,ask_for_offers,,
Expert's Grand Charm,1,,,,,,ask_for_offers,,
Fletcher's Grand Charm Of Inertia,1,,,,,,ask_for_offers,,
Grim Coil,1,,,,,,ask_for_offers,,
Havoc Touch,1,,,,,,ask_for_offers,,
Insight,1,,No base,,,,ask_for_offers,,
Lapis Small Charm,1,,,,,,ask_for_offers,,
Large Charm Of Inertia,1,,,,,,ask_for_offers,,
Large Charm Of Vita,2,,,,,,ask_for_offers,,
Nagelring,2,,,,,,ask_for_offers,,
Northern Worldstone Shard,1,,,,,,ask_for_offers,,
Order Noose,1,,,,,,ask_for_offers,,
Order Touch,1,,,,,,ask_for_offers,,
Perfect Amethyst,1,,,,,,ask_for_offers,,
Perfect Diamond,1,,,,,,ask_for_offers,,
Perfect Emerald,1,,,,,,ask_for_offers,,
Perfect Ruby,1,,,,,,ask_for_offers,,
Perfect Sapphire,1,,,,,,ask_for_offers,,
Perfect Skull,1,,,,,,ask_for_offers,,
Perfect Topaz,1,,,,,,ask_for_offers,,
Rainbow Facet,3,,,,,,ask_for_offers,,
Rugged Small Charm,1,,,,,,ask_for_offers,,
Seraph's Hymn,1,,,,,,ask_for_offers,,
Shogukusha's Grand Charm Of Greed,1,,,,,,ask_for_offers,,
Sigon's Guard,1,,,,,,ask_for_offers,,
Small Charm Of Balance,1,,,,,,ask_for_offers,,
Small Charm Of Inertia,4,,,,,,ask_for_offers,,
Snake's Small Charm Of Inertia,1,,,,,,ask_for_offers,,
Southern Worldstone Shard,1,,,,,,ask_for_offers,,
Sparking Grand Charm,1,,,,,,ask_for_offers,,
Sparking Grand Charm Of Vita,1,,,,,,ask_for_offers,,
Spirit,2,,No base,,,,ask_for_offers,,
Stealth,1,,No base,,,,ask_for_offers,,
String Of Ears,1,,,,,,ask_for_offers,,
Tal Rasha's Adjudication,1,,,,,,ask_for_offers,,
Tal Rasha's Horadric Crest,2,,,,,,ask_for_offers,,
Tarnhelm,2,,,,,,ask_for_offers,,
The Reaper's Toll,1,,,,,,ask_for_offers,,
Waterwalk,1,,,,,,ask_for_offers,,
Western Worldstone Shard,1,,,,,,ask_for_offers,,
Wraith Master,1,,,,,,ask_for_offers,,
1 name qty base_tier base_item ring_variant amulet_variant ethereal pricing price_amount price_currency
2 Amn Rune 10 ask_for_offers
3 Cham Rune 1 ask_for_offers
4 Dol Rune 9 ask_for_offers
5 El Rune 2 ask_for_offers
6 Eld Rune 2 ask_for_offers
7 Eth Rune 9 ask_for_offers
8 Fal Rune 1 ask_for_offers
9 Hel Rune 2 ask_for_offers
10 Io Rune 2 ask_for_offers
11 Ith Rune 1 ask_for_offers
12 Ko Rune 1 ask_for_offers
13 Lem Rune 2 ask_for_offers
14 Lum Rune 1 ask_for_offers
15 Mal Rune 1 ask_for_offers
16 Nef Rune 4 ask_for_offers
17 Ohm Rune 1 ask_for_offers
18 Ort Rune 23 ask_for_offers
19 Ral Rune 9 ask_for_offers
20 Sol Rune 7 ask_for_offers
21 Tal Rune 17 ask_for_offers
22 Thul Rune 6 ask_for_offers
23 Tir Rune 10 ask_for_offers
24 Um Rune 4 ask_for_offers
25 Amber Grand Charm Of Sustenance 1 ask_for_offers
26 Bitter Brogues 1 ask_for_offers
27 Burning Grand Charm 1 ask_for_offers
28 Coral Small Charm Of Incineration 1 ask_for_offers
29 Death Hold 1 ask_for_offers
30 Death Mask 1 ask_for_offers
31 Deep Worldstone Shard 1 ask_for_offers
32 Dwarf Star 1 ask_for_offers
33 Eagle Torc 1 ask_for_offers
34 Eastern Worldstone Shard 1 ask_for_offers
35 Entrapping Grand Charm 1 ask_for_offers
36 Expert's Grand Charm 1 ask_for_offers
37 Fletcher's Grand Charm Of Inertia 1 ask_for_offers
38 Grim Coil 1 ask_for_offers
39 Havoc Touch 1 ask_for_offers
40 Insight 1 No base ask_for_offers
41 Lapis Small Charm 1 ask_for_offers
42 Large Charm Of Inertia 1 ask_for_offers
43 Large Charm Of Vita 2 ask_for_offers
44 Nagelring 2 ask_for_offers
45 Northern Worldstone Shard 1 ask_for_offers
46 Order Noose 1 ask_for_offers
47 Order Touch 1 ask_for_offers
48 Perfect Amethyst 1 ask_for_offers
49 Perfect Diamond 1 ask_for_offers
50 Perfect Emerald 1 ask_for_offers
51 Perfect Ruby 1 ask_for_offers
52 Perfect Sapphire 1 ask_for_offers
53 Perfect Skull 1 ask_for_offers
54 Perfect Topaz 1 ask_for_offers
55 Rainbow Facet 3 ask_for_offers
56 Rugged Small Charm 1 ask_for_offers
57 Seraph's Hymn 1 ask_for_offers
58 Shogukusha's Grand Charm Of Greed 1 ask_for_offers
59 Sigon's Guard 1 ask_for_offers
60 Small Charm Of Balance 1 ask_for_offers
61 Small Charm Of Inertia 4 ask_for_offers
62 Snake's Small Charm Of Inertia 1 ask_for_offers
63 Southern Worldstone Shard 1 ask_for_offers
64 Sparking Grand Charm 1 ask_for_offers
65 Sparking Grand Charm Of Vita 1 ask_for_offers
66 Spirit 2 No base ask_for_offers
67 Stealth 1 No base ask_for_offers
68 String Of Ears 1 ask_for_offers
69 Tal Rasha's Adjudication 1 ask_for_offers
70 Tal Rasha's Horadric Crest 2 ask_for_offers
71 Tarnhelm 2 ask_for_offers
72 The Reaper's Toll 1 ask_for_offers
73 Waterwalk 1 ask_for_offers
74 Western Worldstone Shard 1 ask_for_offers
75 Wraith Master 1 ask_for_offers