{"id":1164,"date":"2026-01-12T13:43:42","date_gmt":"2026-01-12T13:43:42","guid":{"rendered":"https:\/\/southsunindustries.com\/?page_id=1164"},"modified":"2026-03-03T00:21:42","modified_gmt":"2026-03-03T00:21:42","slug":"ratting-loot-salvage-buyback","status":"publish","type":"page","link":"https:\/\/southsunindustries.com\/index.php\/ratting-loot-salvage-buyback\/","title":{"rendered":"Ratting Loot &amp; Salvage Buyback"},"content":{"rendered":"\n<!--\n  Drop-in snippet: paste this into your page where you want the calculator to appear.\n  It won't touch <html>, <head>, <body>, or global tags\/styles.\n\n  Update:\n  - Commodities are now ELIGIBLE (in addition to the existing ratting loot categories).\n  - Style\/colours\/layout unchanged.\n  - Non-eligible items still pay 0 and show in red at the top.\n  - Still flags non-market items (unpublished \/ no market group) as 0 ISK.\n  - Price source: highest buy in Jita 4-4 (The Forge)\n-->\n<div id=\"jbb-root\" class=\"jbb\">\n  <div class=\"jbb__header\">\n    <h2 class=\"jbb__title\">Ratting Loot &amp; Salvage Buyback Calculator<\/h2>\n    <div class=\"jbb__sub\">\n      <div>1. Use the tool below to calculate your total contract price.<\/div>\n      <div>2. You can now paste from EVE inventory in <b>icon-mode<\/b> <i>or<\/i> <b>list-mode<\/b>.<\/div>\n      <div>3. Create a contract in-game to <b>Fenrir Stargazer<\/b>, and copy the total price from the calculator.<\/div>\n      <div>4. Please set the expiry and time to complete to 7 days.<\/div>\n      <div>5. Contracts can be made from any station in UALX, 8-B00B (Tenerifis) or AOK (Catch).<\/div>\n      <div>6. This service is only available to members of South Sun Industries.<\/div>\n      <div>7. DM <b>Fenrir<\/b> in Discord for any queries.<\/div>\n      Price source: <b>highest buy<\/b> in <b>Jita 4-4<\/b>.\n    <\/div>\n  <\/div>\n\n  <div class=\"jbb__grid\">\n    <section class=\"jbb__card\">\n      <h3 class=\"jbb__h3\">Paste items<\/h3>\n      <div class=\"jbb__hint\">\n        One per line. Examples:\n        <span class=\"jbb__mono\">Titanium Sabot S, 500000<\/span> \u2022\n        <span class=\"jbb__mono\">Titanium Sabot S x 12345<\/span> \u2022\n        <span class=\"jbb__mono\">Titanium Sabot S 2<\/span> \u2022\n        <span class=\"jbb__mono\">Titanium Sabot S\t43\tTitanium Sabot S\tCharge<\/span>\n      <\/div>\n      <textarea\n        class=\"jbb__textarea\"\n        id=\"jbb-input\"\n        placeholder=\"Titanium Sabot S, 500000&#10;Titanium Sabot S x 12000&#10;Titanium Sabot S 2&#10;Titanium Sabot S\t43\tTitanium Sabot S\tCharge\"\n      ><\/textarea>\n    <\/section>\n\n    <section class=\"jbb__card\">\n      <h3 class=\"jbb__h3\">Settings<\/h3>\n\n      <div class=\"jbb__row\">\n        <div class=\"jbb__field\">\n          <label class=\"jbb__label\" for=\"jbb-rate\">Buyback rate (%)<\/label>\n          <input class=\"jbb__input\" id=\"jbb-rate\" type=\"number\" step=\"0.1\" min=\"0\" max=\"100\" value=\"85\">\n          <div class=\"jbb__help\">Payout is this % of Jita buy price.<\/div>\n        <\/div>\n      <\/div>\n\n      <div class=\"jbb__actions\">\n        <button class=\"jbb__btn jbb__btn--primary\" id=\"jbb-run\" type=\"button\">Calculate<\/button>\n        <button class=\"jbb__btn\" id=\"jbb-clear\" type=\"button\">Clear<\/button>\n      <\/div>\n\n      <div class=\"jbb__status\" id=\"jbb-status\"><\/div>\n      <div class=\"jbb__help\" style=\"margin-top:10px\"><\/div>\n    <\/section>\n  <\/div>\n\n  <section class=\"jbb__card\">\n    <h3 class=\"jbb__h3\">Results<\/h3>\n\n    <div class=\"jbb__totalBox\" id=\"jbb-totalBox\">\n      <div class=\"jbb__totalLabel\">Total ISK (Buyback Payout)<\/div>\n\n      <div class=\"jbb__totalRow\">\n        <div class=\"jbb__totalValue\" id=\"jbb-totalValue\">\u2014<\/div>\n        <button class=\"jbb__btn jbb__btn--copy\" id=\"jbb-copy\" type=\"button\" disabled title=\"Copy total to clipboard\">\n          Copy\n        <\/button>\n      <\/div>\n\n      <div class=\"jbb__breakdown\" id=\"jbb-breakdown\" style=\"display:none\"><\/div>\n    <\/div>\n\n    <div class=\"jbb__summary\" id=\"jbb-summary\">No results yet.<\/div>\n  <\/section>\n<\/div>\n\n<style>\n  \/* All styles are scoped under .jbb to avoid messing with your site *\/\n  .jbb{\n    font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;\n    color:#e9eef7;\n  }\n\n  .jbb__header{margin-bottom:12px}\n  .jbb__title{\n    margin:0 0 6px;\n    font-size:22px;\n    line-height:1.2;\n    color:#f6f8ff;\n    letter-spacing:.2px;\n    text-shadow:0 1px 0 rgba(0,0,0,.4);\n  }\n  .jbb__sub{color:rgba(233,238,247,.70);font-size:13px}\n  .jbb__mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;color:rgba(233,238,247,.82)}\n\n  .jbb__grid{display:flex;gap:12px;flex-wrap:wrap;margin:12px 0}\n\n  .jbb__card{\n    border:1px solid rgba(255,255,255,.10);\n    border-radius:12px;\n    padding:14px;\n    background:linear-gradient(180deg, rgba(30,40,54,.92), rgba(18,25,35,.92));\n    box-shadow:\n      0 10px 26px rgba(0,0,0,.25),\n      inset 0 1px 0 rgba(255,255,255,.05);\n    flex:1;\n    min-width:280px;\n  }\n\n  .jbb__h3{margin:0 0 8px;font-size:16px;color:#f6f8ff}\n  .jbb__hint{color:rgba(233,238,247,.64);font-size:12px;margin-bottom:8px}\n\n  .jbb__row{display:flex;gap:12px;flex-wrap:wrap}\n  .jbb__field{flex:1;min-width:220px}\n\n  .jbb__label{display:block;color:rgba(233,238,247,.70);font-size:12px;margin-bottom:6px}\n  .jbb__help{color:rgba(233,238,247,.58);font-size:12px}\n\n  .jbb__textarea,\n  .jbb__input{\n    width:100%;\n    padding:10px;\n    border:1px solid rgba(255,255,255,.12);\n    border-radius:10px;\n    font:inherit;\n    box-sizing:border-box;\n    background:rgba(8,12,18,.45);\n    color:#e9eef7;\n    outline:none;\n    box-shadow:inset 0 1px 0 rgba(255,255,255,.03);\n  }\n\n  .jbb__textarea{min-height:180px;resize:vertical}\n  .jbb__textarea::placeholder{color:rgba(233,238,247,.42)}\n\n  .jbb__textarea:focus,\n  .jbb__input:focus{\n    border-color:rgba(120,170,255,.55);\n    box-shadow:\n      inset 0 1px 0 rgba(255,255,255,.03),\n      0 0 0 4px rgba(120,170,255,.14);\n  }\n\n  .jbb__actions{display:flex;gap:10px;flex-wrap:wrap;margin-top:12px}\n\n  .jbb__btn{\n    padding:10px 14px;\n    border:1px solid rgba(255,255,255,.14);\n    border-radius:10px;\n    background:rgba(8,12,18,.35);\n    color:#e9eef7;\n    cursor:pointer;\n    transition:transform .06s ease, border-color .15s ease, box-shadow .15s ease, background .15s ease;\n  }\n  .jbb__btn:hover{\n    background:rgba(8,12,18,.48);\n    border-color:rgba(255,255,255,.22);\n  }\n  .jbb__btn:active{transform:translateY(1px)}\n  .jbb__btn:disabled{opacity:.6;cursor:not-allowed}\n\n  .jbb__btn--primary{\n    background:linear-gradient(180deg, rgba(118,168,255,.92), rgba(84,132,225,.92));\n    border-color:rgba(118,168,255,.55);\n    color:#06111f;\n    font-weight:700;\n    box-shadow:0 10px 22px rgba(118,168,255,.18);\n  }\n  .jbb__btn--primary:hover{\n    border-color:rgba(118,168,255,.75);\n    box-shadow:0 12px 26px rgba(118,168,255,.24);\n  }\n\n  .jbb__status{color:rgba(233,238,247,.62);font-size:12px;margin-top:10px;min-height:16px}\n  .jbb__summary{color:rgba(233,238,247,.62);font-size:12px;margin-top:10px}\n\n  .jbb__totalBox{\n    border:1px solid rgba(255,255,255,.10);\n    border-radius:12px;\n    padding:14px;\n    background:rgba(8,12,18,.25);\n    box-shadow:inset 0 1px 0 rgba(255,255,255,.04);\n  }\n  .jbb__totalLabel{\n    font-size:12px;\n    color:rgba(233,238,247,.62);\n    margin-bottom:6px;\n  }\n\n  .jbb__totalRow{\n    display:flex;\n    align-items:center;\n    gap:10px;\n    justify-content:space-between;\n    flex-wrap:wrap;\n  }\n\n  .jbb__totalValue{\n    font-size:20px;\n    font-weight:800;\n    color:#f6f8ff;\n    letter-spacing:.2px;\n    text-shadow:0 1px 0 rgba(0,0,0,.4);\n  }\n\n  .jbb__btn--copy{\n    padding:8px 12px;\n    font-weight:700;\n    border-color:rgba(255,255,255,.18);\n  }\n  .jbb__btn--copy:hover{\n    border-color:rgba(118,168,255,.40);\n    box-shadow:0 0 0 4px rgba(120,170,255,.10);\n  }\n\n  .jbb__breakdown{\n    margin-top:12px;\n    border-top:1px solid rgba(255,255,255,.08);\n    padding-top:10px;\n  }\n  .jbb__line{\n    display:flex;\n    justify-content:space-between;\n    gap:12px;\n    padding:6px 0;\n    border-bottom:1px dashed rgba(255,255,255,.06);\n  }\n  .jbb__line:last-child{border-bottom:0}\n  .jbb__lineLeft{min-width:0}\n  .jbb__itemName{\n    font-weight:650;\n    color:rgba(246,248,255,.92);\n    font-size:13px;\n    white-space:nowrap;\n    overflow:hidden;\n    text-overflow:ellipsis;\n    max-width:520px;\n  }\n  .jbb__meta{\n    color:rgba(233,238,247,.58);\n    font-size:12px;\n    margin-top:2px;\n  }\n  .jbb__amt{\n    font-weight:750;\n    color:rgba(233,238,247,.92);\n    font-size:13px;\n    text-align:right;\n    white-space:nowrap;\n  }\n\n  .jbb__warnText{\n    color:rgba(255,241,204,.92);\n    font-size:12px;\n  }\n  .jbb__errText{\n    color:rgba(255,220,220,.92);\n    font-size:12px;\n  }\n\n  \/* hard red flag for rejected items (name + amount) *\/\n  .jbb__bad .jbb__itemName,\n  .jbb__bad .jbb__amt{\n    color:rgba(255,120,120,.98);\n  }\n<\/style>\n\n<script>\n(() => {\n  const root = document.getElementById(\"jbb-root\");\n  if (!root) return;\n\n  const ESI = \"https:\/\/esi.evetech.net\/latest\";\n  const REGION_FORGE = 10000002;           \/\/ The Forge\n  const STATION_JITA_44 = 60003760;        \/\/ Jita IV - Moon 4 - Caldari Navy Assembly Plant\n\n  const CACHE_KEY_TYPEIDS = \"jbb_typeid_cache_v2\";\n  const CACHE_KEY_TYPEINFO = \"jbb_typeinfo_cache_v1\";     \/\/ typeId -> {published, market_group_id, group_id}\n  const CACHE_KEY_GROUPINFO = \"jbb_groupinfo_cache_v1\";   \/\/ groupId -> {category_id}\n  const CACHE_KEY_CATINFO = \"jbb_catinfo_cache_v1\";       \/\/ categoryId -> {name}\n\n  \/\/ Allowed EVE category names (case-insensitive)\n  \/\/ NOTE:\n  \/\/ - In-game \"Salvaged Materials\" maps to ESI category name \"Material\"\n  \/\/ - We additionally allow \"Commodities\" per your request\n  const ALLOWED_CATEGORY_NAMES = new Set([\n  \"module\",\n  \"charge\",\n  \"implant\",\n  \"material\",\n  \"salvaged materials\",\n  \"commodity\",     \/\/ <-- ESI uses this\n  \"commodities\"    \/\/ <-- keep as a defensive alias\n]);\n\n  const $ = (sel) => root.querySelector(sel);\n  const input = $(\"#jbb-input\");\n  const rateEl = $(\"#jbb-rate\");\n  const runBtn = $(\"#jbb-run\");\n  const clearBtn = $(\"#jbb-clear\");\n  const copyBtn = $(\"#jbb-copy\");\n  const statusEl = $(\"#jbb-status\");\n  const summaryEl = $(\"#jbb-summary\");\n  const totalValueEl = $(\"#jbb-totalValue\");\n  const breakdownEl = $(\"#jbb-breakdown\");\n\n  const fmt = (n) => (Number.isFinite(n) ? n.toLocaleString(\"en-GB\", {maximumFractionDigits: 2}) : \"\u2014\");\n  const fmtISK0 = (n) => Number.isFinite(n) ? `${Math.round(n).toLocaleString(\"en-GB\")}` : \"\u2014\";\n  const fmtISK2 = (n) => Number.isFinite(n) ? `${n.toLocaleString(\"en-GB\", {maximumFractionDigits: 2})}` : \"\u2014\";\n  const setStatus = (msg) => { statusEl.textContent = msg || \"\"; };\n\n  const cacheTypeIds = {\n    ids: JSON.parse(localStorage.getItem(CACHE_KEY_TYPEIDS) || \"{}\"),\n    save() { localStorage.setItem(CACHE_KEY_TYPEIDS, JSON.stringify(this.ids)); }\n  };\n\n  const cacheTypeInfo = {\n    byId: JSON.parse(localStorage.getItem(CACHE_KEY_TYPEINFO) || \"{}\"),\n    save() { localStorage.setItem(CACHE_KEY_TYPEINFO, JSON.stringify(this.byId)); }\n  };\n\n  const cacheGroupInfo = {\n    byId: JSON.parse(localStorage.getItem(CACHE_KEY_GROUPINFO) || \"{}\"),\n    save() { localStorage.setItem(CACHE_KEY_GROUPINFO, JSON.stringify(this.byId)); }\n  };\n\n  const cacheCatInfo = {\n    byId: JSON.parse(localStorage.getItem(CACHE_KEY_CATINFO) || \"{}\"),\n    save() { localStorage.setItem(CACHE_KEY_CATINFO, JSON.stringify(this.byId)); }\n  };\n\n  let lastTotalPayout = null;\n\n  function escapeHtml(s) {\n    return String(s).replace(\/[&<>\"']\/g, c => ({ \"&\":\"&amp;\",\"<\":\"&lt;\",\">\":\"&gt;\",'\"':\"&quot;\",\"'\":\"&#39;\" }[c]));\n  }\n\n  function parseQty(raw) {\n    if (raw == null) return NaN;\n    let s = String(raw).trim();\n    if (!s) return NaN;\n    s = s.replace(\/\\s+\/g, \"\");\n    s = s.replace(\/,\/g, \"\");\n    const n = Number(s);\n    return Number.isFinite(n) ? n : NaN;\n  }\n\n  \/\/ EVE list-mode: Name \\t Qty \\t Group \\t Category (tabs)\n  function parseEveListModeLine(line) {\n    const cols = line.split(\/\\t+\/).map(c => c.trim()).filter(c => c.length > 0);\n    if (cols.length < 2) return null;\n\n    const name = cols[0];\n    const qty = parseQty(cols[1]);\n    if (!name || !Number.isFinite(qty) || qty <= 0) return null;\n\n    return { name, qty };\n  }\n\n  function parseLines(text) {\n    const lines = text.split(\/\\r?\\n\/).map(s => s.trim()).filter(Boolean);\n    const items = [];\n\n    for (const line of lines) {\n      const lm = parseEveListModeLine(line);\n      if (lm) { items.push(lm); continue; }\n\n      let name = line;\n      let qty = 1;\n\n      \/\/ \"Name, qty\"\n      const comma = line.split(\",\");\n      if (comma.length >= 2) {\n        name = comma.slice(0, -1).join(\",\").trim();\n        qty = parseQty(comma[comma.length - 1].trim());\n      } else {\n        \/\/ \"Name x qty\"\n        const xMatch = line.match(\/^(.*?)(?:\\s*[xX]\\s*)(\\d[\\d\\s,]*(?:\\.\\d+)?)\\s*$\/);\n        if (xMatch) {\n          name = xMatch[1].trim();\n          qty = parseQty(xMatch[2]);\n        } else {\n          \/\/ \"Name qty\" (whitespace number at end)\n          const endNum = line.match(\/^(.*?)[\\s\\t]+(\\d[\\d\\s,]*(?:\\.\\d+)?)\\s*$\/);\n          if (endNum) {\n            name = endNum[1].trim();\n            qty = parseQty(endNum[2]);\n          }\n        }\n      }\n\n      if (!name) continue;\n      if (!Number.isFinite(qty) || qty <= 0) qty = 1;\n      items.push({ name, qty });\n    }\n\n    \/\/ merge duplicates\n    const merged = new Map();\n    for (const it of items) merged.set(it.name, (merged.get(it.name) || 0) + it.qty);\n    return [...merged.entries()].map(([name, qty]) => ({ name, qty }));\n  }\n\n  async function postJSON(url, body) {\n    const res = await fetch(url, {\n      method: \"POST\",\n      headers: { \"Content-Type\": \"application\/json\", \"Accept\": \"application\/json\" },\n      body: JSON.stringify(body)\n    });\n    if (!res.ok) throw new Error(`HTTP ${res.status}`);\n    return res.json();\n  }\n\n  async function getJSON(url) {\n    const res = await fetch(url, { headers: { \"Accept\": \"application\/json\" } });\n    if (!res.ok) throw new Error(`HTTP ${res.status}`);\n    return res.json();\n  }\n\n  async function resolveTypeIds(names) {\n    const missing = names.filter(n => !cacheTypeIds.ids[n]);\n    if (missing.length === 0) return Object.fromEntries(names.map(n => [n, cacheTypeIds.ids[n]]));\n\n    const chunkSize = 150;\n    for (let i = 0; i < missing.length; i += chunkSize) {\n      const chunk = missing.slice(i, i + chunkSize);\n      const data = await postJSON(`${ESI}\/universe\/ids\/`, chunk);\n\n      const inv = {};\n      if (Array.isArray(data.inventory_types)) {\n        for (const t of data.inventory_types) inv[t.name] = t.id;\n      }\n      for (const n of chunk) if (inv[n]) cacheTypeIds.ids[n] = inv[n];\n      cacheTypeIds.save();\n    }\n    return Object.fromEntries(names.map(n => [n, cacheTypeIds.ids[n] || null]));\n  }\n\n  async function getTypeInfo(typeId) {\n    const k = String(typeId);\n    if (cacheTypeInfo.byId[k]) return cacheTypeInfo.byId[k];\n\n    const t = await getJSON(`${ESI}\/universe\/types\/${typeId}\/`);\n    const info = {\n      published: !!t.published,\n      market_group_id: (typeof t.market_group_id === \"number\") ? t.market_group_id : null,\n      group_id: (typeof t.group_id === \"number\") ? t.group_id : null\n    };\n\n    cacheTypeInfo.byId[k] = info;\n    cacheTypeInfo.save();\n    return info;\n  }\n\n  async function getGroupInfo(groupId) {\n    const k = String(groupId);\n    if (cacheGroupInfo.byId[k]) return cacheGroupInfo.byId[k];\n\n    const g = await getJSON(`${ESI}\/universe\/groups\/${groupId}\/`);\n    const info = { category_id: (typeof g.category_id === \"number\") ? g.category_id : null };\n    cacheGroupInfo.byId[k] = info;\n    cacheGroupInfo.save();\n    return info;\n  }\n\n  async function getCategoryInfo(categoryId) {\n    const k = String(categoryId);\n    if (cacheCatInfo.byId[k]) return cacheCatInfo.byId[k];\n\n    const c = await getJSON(`${ESI}\/universe\/categories\/${categoryId}\/`);\n    const info = { name: (c && c.name) ? String(c.name) : \"\" };\n    cacheCatInfo.byId[k] = info;\n    cacheCatInfo.save();\n    return info;\n  }\n\n  \/\/ Accepts ratting loot categories + commodities (and still requires market item)\n  async function validateRattingLoot(typeId) {\n    const t = await getTypeInfo(typeId);\n    if (!t.published) return { ok:false, reason:\"Not a published item (non-market \/ special item).\" };\n    if (!t.market_group_id) return { ok:false, reason:\"Not a market item (no market group).\" };\n    if (!t.group_id) return { ok:false, reason:\"Missing group\/category info (not accepted).\" };\n\n    const g = await getGroupInfo(t.group_id);\n    if (!g.category_id) return { ok:false, reason:\"Missing category info (not accepted).\" };\n\n    const c = await getCategoryInfo(g.category_id);\n    const catName = (c.name || \"\").trim().toLowerCase();\n\n    if (!ALLOWED_CATEGORY_NAMES.has(catName)) {\n      return { ok:false, reason:`Not an eligible category: ${c.name || \"Unknown\"}.` };\n    }\n\n    return { ok:true, categoryName:c.name || \"\" };\n  }\n\n  async function fetchJita44HighestBuy(typeId) {\n    let page = 1;\n    let highest = -Infinity;\n    let foundInForge = false;\n\n    while (true) {\n      const url = `${ESI}\/markets\/${REGION_FORGE}\/orders\/?order_type=buy&type_id=${typeId}&page=${page}`;\n      const res = await fetch(url, { headers: { \"Accept\": \"application\/json\" } });\n      if (!res.ok) throw new Error(`Orders HTTP ${res.status}`);\n\n      const orders = await res.json();\n      if (!Array.isArray(orders) || orders.length === 0) break;\n\n      for (const o of orders) {\n        if (!o || typeof o.price !== \"number\") continue;\n        foundInForge = true;\n        if (o.location_id === STATION_JITA_44 && o.price > highest) highest = o.price;\n      }\n\n      const xPages = res.headers.get(\"x-pages\");\n      const maxPages = xPages ? Number(xPages) : null;\n      page += 1;\n      if (maxPages && page > maxPages) break;\n      if (!maxPages && orders.length < 1000) break;\n    }\n\n    if (highest === -Infinity) {\n      return {\n        price: null,\n        note: foundInForge\n          ? \"No buy orders in Jita 4-4\"\n          : \"No buy orders found in The Forge\"\n      };\n    }\n\n    return { price: highest, note: \"Jita 4-4 highest buy\" };\n  }\n\n  function renderBreakdown(lines) {\n    if (!lines.length) {\n      breakdownEl.style.display = \"none\";\n      breakdownEl.innerHTML = \"\";\n      return;\n    }\n    breakdownEl.style.display = \"\";\n    breakdownEl.innerHTML = lines.map(l => {\n      const cls = `jbb__line ${l.bad ? \"jbb__bad\" : \"\"}`;\n      const left = `\n        <div class=\"jbb__lineLeft\">\n          <div class=\"jbb__itemName\">${escapeHtml(l.name)}<\/div>\n          <div class=\"jbb__meta\">${escapeHtml(l.meta)}<\/div>\n          ${l.noteKind ? `<div class=\"${l.noteKind === \"err\" ? \"jbb__errText\" : \"jbb__warnText\"}\">${escapeHtml(l.note)}<\/div>` : \"\"}\n        <\/div>\n      `;\n      const right = `<div class=\"jbb__amt\">${escapeHtml(l.amount)}<\/div>`;\n      return `<div class=\"${cls}\">${left}${right}<\/div>`;\n    }).join(\"\");\n  }\n\n  async function copyTotalToClipboard() {\n    if (!Number.isFinite(lastTotalPayout)) return;\n    const text = fmtISK0(lastTotalPayout);\n\n    try {\n      if (navigator.clipboard && navigator.clipboard.writeText) {\n        await navigator.clipboard.writeText(text);\n      } else {\n        const ta = document.createElement(\"textarea\");\n        ta.value = text;\n        ta.setAttribute(\"readonly\", \"\");\n        ta.style.position = \"fixed\";\n        ta.style.left = \"-9999px\";\n        ta.style.top = \"0\";\n        document.body.appendChild(ta);\n        ta.select();\n        document.execCommand(\"copy\");\n        document.body.removeChild(ta);\n      }\n\n      const prev = copyBtn.textContent;\n      copyBtn.textContent = \"Copied!\";\n      setTimeout(() => { copyBtn.textContent = prev; }, 1200);\n    } catch (e) {\n      setStatus(\"Copy failed (browser blocked clipboard).\");\n    }\n  }\n\n  async function run() {\n    const items = parseLines(input.value);\n    if (items.length === 0) { setStatus(\"Paste at least one item line.\"); return; }\n\n    const rate = Number(rateEl.value);\n    const rateMult = (Number.isFinite(rate) ? rate : 0) \/ 100;\n\n    runBtn.disabled = true;\n    copyBtn.disabled = true;\n\n    setStatus(\"Resolving type IDs\u2026\");\n    const uniqueNames = [...new Set(items.map(i => i.name))];\n    const nameToId = await resolveTypeIds(uniqueNames);\n\n    let done = 0;\n    let totalPayout = 0;\n\n    let unknownNames = 0;\n    let rejected = 0;\n    let missingJita = 0;\n\n    \/\/ Build in two buckets so rejected always render first\n    const badLines = [];\n    const goodLines = [];\n\n    for (const it of items) {\n      done += 1;\n      setStatus(`Checking items\u2026 (${done}\/${items.length})`);\n\n      const typeId = nameToId[it.name];\n      if (!typeId) {\n        unknownNames += 1;\n        badLines.push({\n          name: it.name,\n          meta: `Qty: ${fmt(it.qty)} \u2022 Payout: 0`,\n          amount: \"0\",\n          note: \"Unknown item name (pays 0).\",\n          noteKind: \"err\",\n          bad: true\n        });\n        continue;\n      }\n\n      \/\/ Validate eligible categories + market item\n      let accepted = false;\n      try {\n        const v = await validateRattingLoot(typeId);\n        if (!v.ok) {\n          rejected += 1;\n          badLines.push({\n            name: it.name,\n            meta: `Qty: ${fmt(it.qty)} \u2022 Payout: 0`,\n            amount: \"0\",\n            note: `${v.reason} (Pays 0)`,\n            noteKind: \"err\",\n            bad: true\n          });\n          continue;\n        }\n        accepted = true;\n      } catch (e) {\n        rejected += 1;\n        badLines.push({\n          name: it.name,\n          meta: `Qty: ${fmt(it.qty)} \u2022 Payout: 0`,\n          amount: \"0\",\n          note: \"Could not validate item category\/market status (pays 0).\",\n          noteKind: \"err\",\n          bad: true\n        });\n        continue;\n      }\n\n      if (!accepted) continue;\n\n      \/\/ Only now fetch market price\n      try {\n        setStatus(`Fetching Jita buy prices\u2026 (${done}\/${items.length})`);\n        const r = await fetchJita44HighestBuy(typeId);\n\n        if (r.price == null) {\n          missingJita += 1;\n          goodLines.push({\n            name: it.name,\n            meta: `Qty: ${fmt(it.qty)} \u2022 Payout: 0`,\n            amount: \"0\",\n            note: r.note || \"No buy orders in Jita 4-4 (pays 0).\",\n            noteKind: \"warn\",\n            bad: false\n          });\n          continue;\n        }\n\n        const jita = r.price;\n        const payout = (jita * it.qty) * rateMult;\n        totalPayout += payout;\n\n        goodLines.push({\n          name: it.name,\n          meta: `Qty: ${fmt(it.qty)} \u2022 Jita: ${fmtISK2(jita)} \u2022 Rate: ${fmt(rate)}%`,\n          amount: fmtISK0(payout),\n          note: \"\",\n          noteKind: \"\",\n          bad: false\n        });\n      } catch (e) {\n        missingJita += 1;\n        goodLines.push({\n          name: it.name,\n          meta: `Qty: ${fmt(it.qty)} \u2022 Payout: 0`,\n          amount: \"0\",\n          note: \"ESI error fetching orders (pays 0).\",\n          noteKind: \"err\",\n          bad: false\n        });\n      }\n    }\n\n    lastTotalPayout = totalPayout;\n    totalValueEl.textContent = fmtISK0(totalPayout);\n    copyBtn.disabled = !Number.isFinite(lastTotalPayout);\n\n    \/\/ Rejected first\n    const breakdown = badLines.concat(goodLines);\n    renderBreakdown(breakdown);\n\n    const notes = [];\n    if (unknownNames) notes.push(`${unknownNames} unknown item(s) paid 0`);\n    if (rejected) notes.push(`${rejected} non-eligible item(s) paid 0`);\n    if (missingJita) notes.push(`${missingJita} item(s) with no Jita 4-4 buy price paid 0`);\n\n    summaryEl.textContent =\n      notes.length\n        ? `Calculated payout at ${fmt(rate)}% of Jita buy. (${notes.join(\" \u2022 \")})`\n        : `Calculated payout at ${fmt(rate)}% of Jita buy.`;\n\n    setStatus(\"Done.\");\n    runBtn.disabled = false;\n  }\n\n  runBtn.addEventListener(\"click\", () => run().catch(e => {\n    setStatus(`Error: ${String(e.message || e)}`);\n    runBtn.disabled = false;\n  }));\n\n  clearBtn.addEventListener(\"click\", () => {\n    input.value = \"\";\n    lastTotalPayout = null;\n    totalValueEl.textContent = \"\u2014\";\n    breakdownEl.style.display = \"none\";\n    breakdownEl.innerHTML = \"\";\n    summaryEl.textContent = \"No results yet.\";\n    copyBtn.disabled = true;\n    copyBtn.textContent = \"Copy\";\n    setStatus(\"\");\n  });\n\n  copyBtn.addEventListener(\"click\", () => copyTotalToClipboard());\n})();\n<\/script>\n","protected":false},"excerpt":{"rendered":"<p> [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"_price":"","_stock":"","_tribe_ticket_header":"","_tribe_default_ticket_provider":"","_ticket_start_date":"","_ticket_end_date":"","_tribe_ticket_show_description":"","_tribe_ticket_show_not_going":false,"_tribe_ticket_use_global_stock":"","_tribe_ticket_global_stock_level":"","_global_stock_mode":"","_global_stock_cap":"","_tribe_rsvp_for_event":"","_tribe_ticket_going_count":"","_tribe_ticket_not_going_count":"","_tribe_tickets_list":"[]","_tribe_ticket_has_attendee_info_fields":false,"footnotes":"","_tec_slr_enabled":"","_tec_slr_layout":""},"class_list":["post-1164","page","type-page","status-publish","hentry"],"ticketed":false,"_links":{"self":[{"href":"https:\/\/southsunindustries.com\/index.php\/wp-json\/wp\/v2\/pages\/1164","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/southsunindustries.com\/index.php\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/southsunindustries.com\/index.php\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/southsunindustries.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/southsunindustries.com\/index.php\/wp-json\/wp\/v2\/comments?post=1164"}],"version-history":[{"count":6,"href":"https:\/\/southsunindustries.com\/index.php\/wp-json\/wp\/v2\/pages\/1164\/revisions"}],"predecessor-version":[{"id":1270,"href":"https:\/\/southsunindustries.com\/index.php\/wp-json\/wp\/v2\/pages\/1164\/revisions\/1270"}],"wp:attachment":[{"href":"https:\/\/southsunindustries.com\/index.php\/wp-json\/wp\/v2\/media?parent=1164"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}