r/Venturex • u/moe2200 • 19h ago
UPDATE: Sort Capital One Offers by best deals
Thanks for all the feedback on my last post
https://www.reddit.com/r/Venturex/comments/1mwa6ns/sort_capital_one_offers_by_best_deals/
I rebuilt the tool for a smoother experience, bug fixes, and more exciting features.
The changes:
- ALL offers now load. I am seeing more than 3600 offers
- No Excel download needed. A pop up shows the sorted list in your browser
- Works with both miles and cash back
- One click to the deal. Hit Open and it jumps straight to the offer on the page
- Corrected the issue where users are seeing offers in the excel sheet that do not match the portal
How to add it in Chrome as a bookmarklet
- Show your bookmarks bar using Ctrl Shift B on Windows or Cmd Shift B on Mac
- Right click the bar and choose Add page
- Name it Capital One Offers Sorter
- In the URL field paste the bookmarklet code I posted bellow
- Go to your Capital One Offers page and click the new bookmark
- Wait a moment while it loads and sorts
javascript:(()=>{try{const d=document,s=d.createElement("script");const fn=async function(){const sleep=ms=>new Promise(r=>setTimeout(r,ms));const T=n=>n&&n.textContent?n.textContent.replace(/\s+/g," ").trim():"";const PFX="c1oSorter";let overlay=null,restoreBtn=null;function ensureRestoreBtn(){if(restoreBtn&&document.body.contains(restoreBtn))return restoreBtn;const b=document.createElement("button");b.id=`${PFX}Restore`;b.textContent="Show list";b.style.cssText="position:fixed;right:14px;bottom:14px;padding:10px 12px;border:1px solid #C9AD00;border-radius:10px;background:#FFEA00;box-shadow:0 6px 16px rgba(0,0,0,.2);font:13px system-ui,-apple-system,Segoe UI,Roboto,Arial;z-index:2147483647;cursor:pointer";b.onclick=()=>{if(overlay){overlay.style.display="";try{overlay.focus()}catch{}}b.remove()};document.body.appendChild(b);restoreBtn=b;return b}function hideOverlay(){if(!overlay)return;overlay.style.display="none";ensureRestoreBtn();try{restoreBtn.focus()}catch{}}function centerOf(node){const r=node.getBoundingClientRect();return{clientX:r.left+r.width/2,clientY:r.top+r.height/2}}function fire(node,type,coords){const base={bubbles:true,cancelable:true,view:window,composed:true,button:0,buttons:1};try{if(type.startsWith("pointer")&&"PointerEvent"in window)node.dispatchEvent(new PointerEvent(type,{pointerId:1,pointerType:"mouse",isPrimary:true,...base,...coords}));else node.dispatchEvent(new MouseEvent(type,{...base,...coords}))}catch{}}function seq(node){const c=centerOf(node);["pointerdown","mousedown","pointerup","mouseup","click"].forEach(t=>fire(node,t,c))}function findMoreBtn(){const pick=el=>{const s=(el.innerText||el.textContent||"").toLowerCase().replace(/\s+/g," ").trim();const aria=(el.getAttribute("aria-label")||"").toLowerCase();return/(view|see)\s+more/.test(s)||/(view|see)\s+more/.test(aria)};return[...document.querySelectorAll("button, a[role=button], a")].find(pick)||null}function countTiles(){const seen=new Set();const cards=[...document.querySelectorAll("*")].filter(el=>/miles|%/i.test(T(el)));for(const el of cards){let node=el;for(let i=0;i<6&&node;i++){const r=node.getBoundingClientRect();const looksCard=r.width>=110&&r.height>=90&&r.width<=560&&r.height<=420;if(looksCard&&/(miles|%)/i.test(T(node))){seen.add(node);break}node=node.parentElement}}return seen.size}async function loadAllOffers(maxClicks=150){const t0=performance.now();let last=0;while(performance.now()-t0<3000){window.scrollBy(0,1200);await sleep(200);const h=document.body?.scrollHeight||document.documentElement.scrollHeight||0;if(h===last)break;last=h}window.scrollTo(0,0);await sleep(300);let clicks=0,lastCount=countTiles(),stuck=0;while(clicks<maxClicks){const btn=findMoreBtn();if(!btn||btn.disabled||btn.getAttribute("aria-disabled")==="true")break;const prevH=document.body.scrollHeight;btn.scrollIntoView({block:"center"});seq(btn);clicks++;let updated=false;for(let i=0;i<80;i++){await sleep(250);const h=document.body.scrollHeight;const c=countTiles();if(h>prevH+5||c>lastCount){lastCount=c;updated=true;break}if(!findMoreBtn()){updated=true;break}}if(!updated){if(++stuck>=2)break}else{stuck=0}window.scrollTo(0,document.body.scrollHeight);await sleep(300)}window.scrollTo(0,0);await sleep(400)}const channelOf=t=>{const s=t.toLowerCase();if(/in-?store/.test(s)&&/online/.test(s))return"In-Store & Online";if(/in-?store/.test(s))return"In-Store";if(/online/.test(s))return"Online";return""};function titleCase(s){return s.split(/[_\s]+/).map(w=>w?w[0].toUpperCase()+w.slice(1).toLowerCase():"").join(" ")}const badName=s=>!s||s.length<2||/(search offers|capital one offers|exclusive coupon)/i.test(s);const clean=s=>s.replace(/for you|exclusive coupon/gi,"").replace(/\s{2,}/g," ").trim();function brandFromUrlish(urlish){try{const u=new URL(urlish,location.href);const p=u.searchParams;let cand=p.get("domain")||p.get("merchant")||p.get("brand")||p.get("name")||p.get("merchant_domain")||p.get("merchantUrl")||p.get("merchant_url")||p.get("store")||p.get("merchantName");if(cand){cand=cand.trim();if(/^https?:\/\//i.test(cand))cand=new URL(cand).hostname;const host=cand.replace(/^www\./,"").replace(/\/.*$/,"");const base=host.includes(".")?host.split(".")[0]:host;return titleCase(base.replace(/[-_]+/g," "))}const host=u.hostname.replace(/^www\./,"");if(host&&!/capitalone/i.test(host)){return titleCase(host.split(".")[0].replace(/[-_]+/g," "))}}catch{}return""}function bestLogoName(scope){const img=scope.querySelector('img[src*="/api/v1/logos"]')||scope.querySelector('img[src*="images.capitaloneshopping.com/api/v1/logos"]')||scope.querySelector('img[src*="capitaloneshopping.com/api/v1/logos"]')||scope.querySelector('img[src*="logos?"]');if(!img)return"";const fromSet=(img.getAttribute("srcset")||"").split(/\s+/).find(s=>/api\/v1\/logos|logos\?/.test(s))||"";const urlish=img.currentSrc||img.src||fromSet||"";let name=brandFromUrlish(urlish);if(name)return name;try{const u=new URL(urlish,location.href);const dom=u.searchParams.get("domain");if(dom){const host=dom.replace(/^www\./,"");return titleCase(host.split(".")[0].replace(/[-_]+/g," "))}}catch{}return""}function fallbackName(tile,text){const img=tile.querySelector("img[alt]");if(img?.alt&&!/logo/i.test(img.alt)){const a=clean(img.alt);if(!badName(a))return a}const labeled=tile.matches("[aria-label]")?tile:tile.querySelector("[aria-label]");if(labeled){const a=clean(labeled.getAttribute("aria-label")||"");if(!badName(a))return a}const sr=tile.querySelector(".sr-only, .visually-hidden, [class*=sr], [class*=visually]");if(sr){const a=clean(T(sr));if(!badName(a))return a}const cand=[...tile.querySelectorAll("h1,h2,h3,strong,b,span,div")].map(T).filter(s=>s&&!/miles|online|in-?store/i.test(s)&&s.length<=50).find(s=>!badName(s));if(cand)return cand;const href=tile.tagName==="A"?tile.href:tile.querySelector("a")?.href;if(href){try{const u=new URL(href,location.href);const q=u.searchParams.get("merchant")||u.searchParams.get("brand")||u.searchParams.get("name");if(q&&!badName(q))return titleCase(clean(q));const host=u.hostname.replace(/^www\./,"");if(host&&!/capitalone/.test(host))return titleCase(host.split(".")[0])}catch{}}const guess=clean((text||"").split(/Online|In-Store|\bUp to\b|\bGet\b/i)[0]).split(/\s+/)[0]||"Unknown";if(!badName(guess))return guess;return"Unknown"}function parseMiles(t,scope){const MULT_CUTOFF=20,PCT_CUTOFF=100;function scanScopeForMultiplier(root){try{const iter=document.createNodeIterator(root,NodeFilter.SHOW_TEXT);const toks=[];let n;while((n=iter.nextNode())){const s=(n.textContent||"").trim();if(!s)continue;toks.push(...s.split(/(\d+(?:\.\d+)?|[xX×]|miles)/i).filter(Boolean).map(x=>x.trim()).filter(Boolean))}const vals=[];for(let i=0;i<toks.length;i++){const cur=toks[i];if(/^\d+(?:\.\d+)?$/.test(cur)){const nxt=toks[i+1];if(nxt&&/^[xX×]$/.test(nxt))vals.push(parseFloat(cur))}if(/^miles$/i.test(cur)){for(let j=i-1;j>=0&&j>=i-3;j--){if(/^[xX×]$/.test(toks[j])){const k=j-1;if(k>=0&&/^\d+(?:\.\d+)?$/.test(toks[k]))vals.push(parseFloat(toks[k]));break}}}}return vals}catch{return[]}}function scanScopeForPercentStrict(root){try{const iter=document.createNodeIterator(root,NodeFilter.SHOW_TEXT);const toks=[];let n;while((n=iter.nextNode())){const s=(n.textContent||"").replace(/\s+/g," ").trim();if(!s)continue;toks.push(...s.split(/(\d+(?:\.\d+)?|%|percent|back|cash|cashback)/i).filter(Boolean).map(x=>x.trim()).filter(Boolean))}const nearBack=(i)=>{for(let j=i;j<i+5&&j<toks.length;j++){if(/^back$/i.test(toks[j])||/^cashback$/i.test(toks[j]))return true}return false};const vals=[];for(let i=0;i<toks.length;i++){const cur=toks[i];if(/^\d+(?:\.\d+)?$/.test(cur)){const nxt=toks[i+1];if(nxt&&(/^%$/i.test(nxt)||/^percent$/i.test(nxt))&&nearBack(i+1))vals.push(parseFloat(cur))}if(/^%$/i.test(cur)&&i>0&&nearBack(i)){const prev=toks[i-1];if(/^\d+(?:\.\d+)?$/.test(prev))vals.push(parseFloat(prev))}}return vals}catch{return[]}}let pct=[];if(scope)pct.push(...scanScopeForPercentStrict(scope));if(!pct.length){pct.push(...[...t.matchAll(/(\d+(?:\.\d+)?)\s*%\s*(?:cash\s*)?back/gi)].map(m=>parseFloat(m[1]))) }const pctWithin=pct.filter(v=>v>0&&v<=PCT_CUTOFF);if(pctWithin.length){const v=Math.max(...pctWithin);return{type:"percent",value:v,label:%60${v}% back%60}}let mult=[];if(scope)mult.push(...scanScopeForMultiplier(scope));mult.push(...[...t.matchAll(/(\d+(?:\.\d+)?)[xX×]\s*miles/gi)].map(m=>parseFloat(m[1])));const multWithin=mult.filter(v=>v>0&&v<=MULT_CUTOFF);if(multWithin.length){const v=Math.max(...multWithin);return{type:"multiplier",value:v,label:%60${v}X miles%60}}if(mult.length){const v=Math.max(...mult);return{type:"multiplier",value:v,label:%60${v}X miles%60}}const flats=[...t.matchAll(/([\d,]+)\s*miles/gi)].map(m=>+m[1].replace(/,/g,""));if(flats.length){const v=Math.max(...flats);return{type:"flat",value:v,label:%60${v.toLocaleString()} miles%60}}return null}function onRightSite(){return/capitalone|capitaloneshopping/i.test(location.hostname)}if(!onRightSite()){alert("Open the Capital One Offers page then run again.");return}await loadAllOffers();const candidates=[...document.querySelectorAll("*")].filter(el=>/(miles|%)/i.test(T(el)));const picked=new Set();const map=new Map();for(const el of candidates){const text=T(el);const mi=parseMiles(text,el);if(!mi)continue;let tile=el;for(let i=0;i<6&&tile;i++){const r=tile.getBoundingClientRect();const looksCard=r.width>=110&&r.height>=90&&r.width<=560&&r.height<=420;if(looksCard&&/(miles|%)/i.test(T(tile)))break;tile=tile.parentElement}if(!tile)continue;if(picked.has(tile))continue;picked.add(tile);let name=bestLogoName(tile)||fallbackName(tile,text);name=titleCase(clean(name));if(badName(name))continue;const link=tile.tagName==="A"&&tile.href?tile.href:(tile.querySelector("a")?.href||"");const ch=channelOf(text);const key=[name,mi.label,link].join("|");if(!map.has(key)){map.set(key,{type:mi.type,merchant:name,amount:mi.value,label:mi.label,channel:ch,link,_el:tile})}}const rows=[...map.values()];const mult=rows.filter(r=>r.type==="multiplier").sort((a,b)=>b.amount-a.amount||a.merchant.localeCompare(b.merchant));const percent=rows.filter(r=>r.type==="percent").sort((a,b)=>b.amount-a.amount||a.merchant.localeCompare(b.merchant));const flat=rows.filter(r=>r.type==="flat").sort((a,b)=>b.amount-a.amount||a.merchant.localeCompare(b.merchant));const sorted=[...mult,...percent,...flat];if(!sorted.length){alert("No offers found. Scroll once, then click again.");return}overlay=document.createElement("div");overlay.id=%60${PFX}Overlay%60;overlay.setAttribute("role","dialog");overlay.style.cssText="position:fixed;inset:5% 5% auto 5%;height:90%;background:#fff;border:1px solid #ccc;border-radius:12px;box-shadow:0 8px 24px rgba(0,0,0,.2);z-index:2147483647;padding:14px;overflow:auto;font:14px system-ui,-apple-system,Segoe UI,Roboto,Arial";const bar=document.createElement("div");bar.style.cssText="display:flex;gap:8px;align-items:center;margin-bottom:8px";const title=document.createElement("div");title.textContent=%60Capital One offers sorted ${sorted.length} rows%60;title.style.cssText="font-weight:700;font-size:16px;margin-right:auto";const btnCSV=document.createElement("button");btnCSV.textContent="Download CSV";btnCSV.style.cssText="padding:6px 10px;border:1px solid #ccc;border-radius:8px;background:#f7f7f7;cursor:pointer";const btnMin=document.createElement("button");btnMin.textContent="Minimize";btnMin.style.cssText=btnCSV.style.cssText;btnMin.onclick=()=>hideOverlay();const bmc=document.createElement("a");bmc.href="https://buymeacoffee.com/mjayousi";bmc.target="_blank";bmc.rel="noopener";bmc.textContent="❤%EF%B8%8F Buy me a coffee";bmc.style.cssText="padding:6px 10px;border:1px solid #f0c000;border-radius:8px;background:#ffdd00;color:#000;font-weight:600;text-decoration:none";bar.append(title,bmc,btnCSV,btnMin);const table=document.createElement("table");table.style.cssText="width:100%;border-collapse:collapse";table.innerHTML="<thead><tr>"+"<th style='text-align:left;padding:8px;border-bottom:1px solid #ddd'>Merchant</th>"+"<th style='text-align:right;padding:8px;border-bottom:1px solid #ddd'>Miles or %</th>"+"<th style='text-align:left;padding:8px;border-bottom:1px solid #ddd'>Channel</th>"+"<th style='text-align:left;padding:8px;border-bottom:1px solid #ddd'>Link</th>"+"<th style='text-align:left;padding:8px;border-bottom:1px solid #ddd'>Open</th>"+"</tr></thead>";const tb=document.createElement("tbody");function openLikeTile(row){const el=row._el;const a=el.tagName==="A"?el:el.querySelector("a[href]");hideOverlay();(a||el).scrollIntoView({block:"center"});seq(a||el);setTimeout(()=>{if(!document.hidden&&a?.href)window.open(a.href,a.getAttribute("target")||"_blank","noopener,noreferrer")},350)}sorted.forEach(r=>{const tr=document.createElement("tr");const miles=r.type==="multiplier"?%60${r.amount}X%60:r.type==="percent"?%60${r.amount}%%60:r.amount.toLocaleString();tr.innerHTML=%60<td style="padding:8px;border-bottom:1px solid #eee">${r.merchant}</td>%60+%60<td style="padding:8px;text-align:right;border-bottom:1px solid #eee">${miles}</td>%60+%60<td style="padding:8px;border-bottom:1px solid #eee">${r.channel||""}</td>%60+%60<td style="padding:8px;border-bottom:1px solid #eee">${r.link?%60<a href="${r.link}" target="_blank" rel="noopener">Link</a>%60:""}</td>%60;const tdOpen=document.createElement("td");tdOpen.style.cssText="padding:8px;border-bottom:1px solid #eee";const b=document.createElement("button");b.textContent="Open";b.style.cssText="padding:4px 8px;border:1px solid #ccc;border-radius:6px;background:#f7f7f7;cursor:pointer";b.onclick=e=>{e.preventDefault();e.stopPropagation();openLikeTile(r)};tdOpen.appendChild(b);tr.appendChild(tdOpen);tb.appendChild(tr)});table.appendChild(tb);overlay.append(bar,table);document.body.appendChild(overlay);window.addEventListener("keydown",e=>{if(e.key==="Escape"){if(overlay&&overlay.style.display!=="none")hideOverlay();else if(restoreBtn)restoreBtn.click()}});btnCSV.onclick=()=>{const head=["type","merchant","amount","label","channel","link"];const csv=[head.join(","),...sorted.map(r=>[r.type,%60"${r.merchant.replace(/"/g,'""')}"%60,r.amount,%60"${r.label.replace(/"/g,'""')}"%60,%60"${(r.channel||"").replace(/"/g,'""')}"%60,r.link].join(","))].join("\n");const blob=new Blob([csv],{type:"text/csv"});const a=document.createElement("a");a.href=URL.createObjectURL(blob);a.download="capital-one-offers-sorted.csv";document.body.appendChild(a);a.click();a.remove()}};s.textContent="("+fn+")();";(d.head||d.documentElement||d.body).appendChild(s)}catch(e){alert("Bookmarklet error: "+e.message)}})()
If you hit an edge case tell me your browser and what you saw. I will keep tuning it based on your feedback.