← back to AbramsOS
settlements: live openclaw auto-fill runner (dedicated tab, gateway click-through, proximity+single-use matcher, state-select) + /run endpoint + Go button triggers zero-click fill w/ screenshot; paste-card fallback
b3c0e91395c6ac122f8a31ccb85d5ef91acbbec6 · 2026-08-18 16:09:52 -0700 · Steve
Files touched
A lib/claim-openclaw-run.jsM routes/settlements.jsM views/settlements.ejs
Diff
commit b3c0e91395c6ac122f8a31ccb85d5ef91acbbec6
Author: Steve <steve@designerwallcoverings.com>
Date: Tue Aug 18 16:09:52 2026 -0700
settlements: live openclaw auto-fill runner (dedicated tab, gateway click-through, proximity+single-use matcher, state-select) + /run endpoint + Go button triggers zero-click fill w/ screenshot; paste-card fallback
---
lib/claim-openclaw-run.js | 144 ++++++++++++++++++++++++++++++++++++++++++++++
routes/settlements.js | 18 ++++++
views/settlements.ejs | 35 ++++++-----
3 files changed, 184 insertions(+), 13 deletions(-)
diff --git a/lib/claim-openclaw-run.js b/lib/claim-openclaw-run.js
new file mode 100644
index 0000000..2974404
--- /dev/null
+++ b/lib/claim-openclaw-run.js
@@ -0,0 +1,144 @@
+// lib/claim-openclaw-run.js — live openclaw auto-TYPE for a settlement claim form.
+// Connects to the already-running openclaw Chrome (CDP :18800) via puppeteer-core and drives
+// its OWN dedicated tab (immune to whatever else is churning the shared browser's active tab).
+// It navigates to the claim form, label-matches the identity + payment fields across ANY
+// administrator's markup, types the values, screenshots the filled form, and STOPS.
+//
+// HARD LINE: never checks the penalty-of-perjury attestation, never clicks Submit/File. The tab
+// is left open for Steve to review, attest, and submit himself.
+
+const path = require('path');
+const fs = require('fs');
+const puppeteer = require('puppeteer-core');
+
+const CDP = process.env.OPENCLAW_CDP || 'http://127.0.0.1:18800';
+const SHOT_DIR = path.join(__dirname, '..', 'public', 'claim-shots');
+
+// The label-matcher runs INSIDE the page. Matches inputs by their label/aria/placeholder/name,
+// fills identity fields, and selects the PayPal payment radio. Never touches submit/attestation.
+function inPageFill(values) {
+ var STATES = { AL:'Alabama',AK:'Alaska',AZ:'Arizona',AR:'Arkansas',CA:'California',CO:'Colorado',CT:'Connecticut',DE:'Delaware',FL:'Florida',GA:'Georgia',HI:'Hawaii',ID:'Idaho',IL:'Illinois',IN:'Indiana',IA:'Iowa',KS:'Kansas',KY:'Kentucky',LA:'Louisiana',ME:'Maine',MD:'Maryland',MA:'Massachusetts',MI:'Michigan',MN:'Minnesota',MS:'Mississippi',MO:'Missouri',MT:'Montana',NE:'Nebraska',NV:'Nevada',NH:'New Hampshire',NJ:'New Jersey',NM:'New Mexico',NY:'New York',NC:'North Carolina',ND:'North Dakota',OH:'Ohio',OK:'Oklahoma',OR:'Oregon',PA:'Pennsylvania',RI:'Rhode Island',SC:'South Carolina',SD:'South Dakota',TN:'Tennessee',TX:'Texas',UT:'Utah',VT:'Vermont',VA:'Virginia',WA:'Washington',WV:'West Virginia',WI:'Wisconsin',WY:'Wyoming' };
+ // checked in this ORDER (specific first); email before street; street excludes email/apt/line-2.
+ var ORDER = ['email','first_name','middle','last_name','zip','city','state','phone','street','full_name'];
+ var KEY = {
+ email: { rx: /e-?mail/i },
+ first_name: { rx: /first\s*(name)?|fname|given/i },
+ middle: { rx: /middle|m\.?i\.?|initial/i },
+ last_name: { rx: /last\s*(name)?|lname|surname|family/i },
+ full_name: { rx: /full\s*name|^\s*name\s*$/i },
+ street: { rx: /(street|mailing|^\s*address\s*$|address\s*1|street\s*1|addr)/i, not: /e-?mail|apartment|unit|suite|\bbox\b|street\s*2|address\s*2|line\s*2/i },
+ city: { rx: /city|town/i },
+ state: { rx: /\bstate\b|province/i },
+ zip: { rx: /zip|postal/i },
+ phone: { rx: /phone|tel(?!e?vision)|mobile/i },
+ };
+ function txt(node) { if (!node) return ''; var c = node.cloneNode(true); var kids = c.querySelectorAll ? c.querySelectorAll('input,select,textarea,button,script,style') : []; for (var j = 0; j < kids.length; j++) kids[j].remove(); return (c.innerText || c.textContent || ''); }
+ function labelFor(el) {
+ var t = '';
+ // 1) standard associations
+ try { if (el.id) { var l = document.querySelector('label[for="' + (window.CSS && CSS.escape ? CSS.escape(el.id) : el.id) + '"]'); if (l) t += ' ' + l.textContent; } } catch (e) {}
+ var w = el.closest && el.closest('label'); if (w) t += ' ' + txt(w);
+ t += ' ' + (el.getAttribute('aria-label') || '') + ' ' + (el.placeholder || '') + ' ' + (el.name || '') + ' ' + (el.id || '');
+ // 2) PROXIMITY — these forms use bare inputs with the label as nearby visible text
+ if (el.previousElementSibling) t += ' ' + txt(el.previousElementSibling);
+ var cell = el.closest && el.closest('td,th,div,li,p,span,section,fieldset');
+ if (cell) t += ' ' + txt(cell);
+ var td = el.closest && el.closest('td'); if (td && td.previousElementSibling) t += ' ' + txt(td.previousElementSibling);
+ // walk back a couple prior siblings for "label then input" layouts
+ var pv = el.previousElementSibling, hop = 0;
+ while (pv && hop < 3) { t += ' ' + txt(pv); pv = pv.previousElementSibling; hop++; }
+ return t.toLowerCase().replace(/\s+/g, ' ').trim().slice(0, 200);
+ }
+ var filled = [];
+ var usedKey = {}; // single-use keys (fill each identity field once)
+ var els = Array.prototype.slice.call(document.querySelectorAll('input, select, textarea'));
+ for (var i = 0; i < els.length; i++) {
+ var el = els[i];
+ var ty = (el.type || '').toLowerCase();
+ if (['hidden', 'submit', 'button', 'checkbox', 'radio', 'file', 'password'].indexOf(ty) >= 0) continue;
+ if (el.value && el.tagName !== 'SELECT') continue; // don't overwrite what's already there
+ var lab = labelFor(el);
+ for (var oi = 0; oi < ORDER.length; oi++) {
+ var k = ORDER[oi];
+ var def = KEY[k];
+ if (usedKey[k]) continue;
+ if (!def.rx.test(lab)) continue;
+ if (def.not && def.not.test(lab)) continue;
+ if (k === 'middle') { usedKey[k] = 1; break; } // claim the MI input so it's not mis-filled; no value passed
+ if (!values[k]) { break; } // matched but nothing to put → leave blank, stop trying other keys
+ if (el.tagName === 'SELECT') {
+ var want = String(values[k]).toLowerCase();
+ var full = (k === 'state' && STATES[String(values[k]).toUpperCase()] || '').toLowerCase();
+ for (var o = 0; o < el.options.length; o++) {
+ var op = el.options[o]; var ov = op.value.toLowerCase(), otx = op.textContent.trim().toLowerCase();
+ if (ov === want || otx === want || (full && (ov === full || otx === full))) { el.value = op.value; break; }
+ }
+ el.dispatchEvent(new Event('change', { bubbles: true }));
+ } else {
+ try { el.focus(); } catch (e) {}
+ el.value = values[k];
+ el.dispatchEvent(new Event('input', { bubbles: true }));
+ el.dispatchEvent(new Event('change', { bubbles: true }));
+ el.dispatchEvent(new Event('blur', { bubbles: true }));
+ }
+ usedKey[k] = 1; filled.push(k);
+ break;
+ }
+ }
+ // payment: select PayPal option if the form has one (radio or its label)
+ var paid = false;
+ var radios = Array.prototype.slice.call(document.querySelectorAll('input[type=radio]'));
+ for (var r = 0; r < radios.length; r++) {
+ var rl = (radios[r].value || '') + ' ' + labelForRadio(radios[r]);
+ if (/paypal/i.test(rl)) { try { radios[r].click(); paid = true; } catch (e) {} break; }
+ }
+ function labelForRadio(el) { var w = el.closest && el.closest('label'); var t = w ? w.textContent : ''; if (el.id) { var l = document.querySelector('label[for="' + el.id + '"]'); if (l) t += ' ' + l.textContent; } return t; }
+ return { filled: filled, paypal_selected: paid, url: location.href, title: document.title };
+}
+
+async function runFill(claimId, formUrl, values) {
+ fs.mkdirSync(SHOT_DIR, { recursive: true });
+ let browser;
+ try {
+ browser = await puppeteer.connect({ browserURL: CDP, defaultViewport: null, protocolTimeout: 60000 });
+ } catch (e) {
+ return { ok: false, error: 'openclaw browser not reachable at ' + CDP + ' (' + e.message + ')' };
+ }
+ let page;
+ try {
+ page = await browser.newPage(); // OUR tab — not the shared active tab
+ await page.goto(formUrl, { waitUntil: 'domcontentloaded', timeout: 45000 });
+ await new Promise((r) => setTimeout(r, 3500)); // let SPA render
+ // follow an obvious "File a Claim / Submit a Claim / Claim Form" link if we landed on a landing page
+ try {
+ const RX = /file a claim|submit a claim|online claim|claim form|start (your )?claim|enter online|online filing|filing site|enter .*filing|proceed to|continue to (the )?claim|begin/i;
+ // Try up to 3 hops of "enter/continue" gateways (instructions page → filing site → form).
+ for (var hop = 0; hop < 3; hop++) {
+ var acted = await page.evaluate((rxSrc) => {
+ var rx = new RegExp(rxSrc, 'i');
+ var el = Array.prototype.slice.call(document.querySelectorAll('a,button,input[type=submit],input[type=button]'))
+ .find(function (e) { return rx.test(e.textContent || e.value || ''); });
+ if (!el) return null;
+ if (el.href && /^https?:/.test(el.href)) return { href: el.href };
+ el.click(); return { clicked: true };
+ }, RX.source);
+ if (!acted) break;
+ if (acted.href) { await page.goto(acted.href, { waitUntil: 'domcontentloaded', timeout: 45000 }); }
+ await new Promise((r) => setTimeout(r, 3500));
+ // stop hopping once we see real name/email inputs
+ var hasForm = await page.evaluate(() => document.querySelectorAll('input[type=text],input[type=email]').length >= 3);
+ if (hasForm) break;
+ }
+ } catch (e) {}
+ const result = await page.evaluate(inPageFill, values);
+ const shot = path.join(SHOT_DIR, claimId + '.png');
+ await page.screenshot({ path: shot, fullPage: true }).catch(() => {});
+ browser.disconnect(); // LEAVE the tab open for Steve to review + attest + submit
+ return { ok: true, filled: result.filled, paypal_selected: result.paypal_selected, on: result.url, title: result.title, screenshot: '/claim-shots/' + claimId + '.png' };
+ } catch (e) {
+ try { browser.disconnect(); } catch (_) {}
+ return { ok: false, error: e.message };
+ }
+}
+
+module.exports = { runFill };
diff --git a/routes/settlements.js b/routes/settlements.js
index 64d9f28..1323b8a 100644
--- a/routes/settlements.js
+++ b/routes/settlements.js
@@ -8,6 +8,7 @@ const db = require('../lib/db');
const filler = require('../lib/openclaw-claim-filler');
const { extractAddress } = require('../lib/claims-autopilot');
const { rankClaims } = require('../lib/settlement-score');
+const { runFill } = require('../lib/claim-openclaw-run');
const router = express.Router();
const USER = 'user_steve';
@@ -119,4 +120,21 @@ router.post('/api/settlements/:id/claim-and-go', async (req, res) => {
} catch (e) { res.status(503).json({ error: 'db offline: ' + e.message }); }
});
+// LIVE auto-fill: open the claim form in the openclaw Chrome and type the identity+payment
+// fields (zero-click for Steve), stopping before the perjury attestation. Never submits.
+router.post('/api/settlements/:id/run', async (req, res) => {
+ try {
+ const r = await db.query(`SELECT * FROM settlement_claim WHERE id=$1 AND user_id=$2`, [req.params.id, USER]);
+ if (!r.rows.length) return res.status(404).json({ error: 'not found' });
+ const claim = r.rows[0];
+ const url = claim.admin_url || claim.mode_url;
+ if (!url) return res.status(400).json({ error: 'no claim URL on record' });
+ await db.query(`UPDATE settlement_claim SET eligibility_state='eligible',updated_at=now() WHERE id=$1`, [claim.id]).catch(() => {});
+ const profile = await loadProfile();
+ const out = await runFill(claim.id, url, fieldMap(profile));
+ if (out.ok) await db.query(`UPDATE settlement_claim SET fill_state='prefilled_awaiting_submit',updated_at=now() WHERE id=$1`, [claim.id]).catch(() => {});
+ res.json(out);
+ } catch (e) { res.status(503).json({ error: e.message }); }
+});
+
module.exports = router;
diff --git a/views/settlements.ejs b/views/settlements.ejs
index 78deb23..16927f0 100644
--- a/views/settlements.ejs
+++ b/views/settlements.ejs
@@ -161,20 +161,29 @@
// Claim it & Go
function esc(s){ return (s||'').replace(/[&<>"]/g,function(c){return{'&':'&','<':'<','>':'>','"':'"'}[c];}); }
function kv(k,v){ if(!v) return ''; return '<div class="kv"><span class="k">'+esc(k)+'</span><span class="v">'+esc(v)+'</span><button onclick="navigator.clipboard.writeText(this.previousSibling.textContent)">copy</button></div>'; }
+ function showPrep(title, note, fields, screenshot){
+ document.getElementById('prep-title').textContent=title;
+ document.getElementById('prep-note').textContent=note||'';
+ var g=document.getElementById('prep-green');
+ if(screenshot){ g.innerHTML='<img src="'+screenshot+'?t='+Date.now()+'" style="width:100%;border-radius:8px;border:1px solid rgba(255,255,255,.1)"><div class="claim-meta" style="margin-top:6px">↑ what was typed into your Chrome. Switch to Chrome to review & submit.</div>'; }
+ else if(fields){ var f=fields; g.innerHTML=kv('First name',f.first_name)+kv('Last name',f.last_name)+kv('Street',f.street)+kv('City',f.city)+kv('State',f.state)+kv('Zip',f.zip)+kv('Email',f.email)+kv('Phone',f.phone)+kv('Payment',f.payment_method+' → '+f.paypal_email); }
+ document.getElementById('prep-open').style.display='none';
+ document.getElementById('prep-bd').style.display='flex';
+ }
document.querySelectorAll('.go').forEach(function(btn){ btn.onclick=function(){
- var id=btn.dataset.id;
- btn.disabled=true; btn.textContent='…';
- fetch('/api/settlements/'+id+'/claim-and-go',{method:'POST'}).then(function(r){return r.json();}).then(function(j){
- btn.disabled=false; btn.textContent='✅ Claim it & Go';
- if(j.error){ alert(j.error); return; }
- var f=j.fields||{};
- document.getElementById('prep-title').textContent=btn.dataset.name||'Claim it & Go';
- document.getElementById('prep-note').textContent=j.note||'';
- document.getElementById('prep-green').innerHTML =
- kv('First name',f.first_name)+kv('Last name',f.last_name)+kv('Street',f.street)+kv('City',f.city)+kv('State',f.state)+kv('Zip',f.zip)+kv('Email',f.email)+kv('Phone',f.phone)+kv('Payment',f.payment_method+' → '+f.paypal_email);
- var open=document.getElementById('prep-open');
- if(j.mode_url){ open.href=j.mode_url; open.style.display=''; window.open(j.mode_url,'_blank','noopener'); } else { open.style.display='none'; }
- document.getElementById('prep-bd').style.display='flex';
+ var id=btn.dataset.id, name=btn.dataset.name||'Claim';
+ btn.disabled=true; btn.textContent='🤖 auto-filling in Chrome…';
+ // 1) live openclaw auto-fill (zero-click)
+ fetch('/api/settlements/'+id+'/run',{method:'POST'}).then(function(r){return r.json();}).then(function(j){
+ if(j && j.ok){ btn.disabled=false; btn.textContent='✅ Filled — review in Chrome';
+ showPrep(name, 'Auto-filled '+((j.filled||[]).length)+' fields in your Chrome ('+(j.title||'')+'). Switch to Chrome, verify, check the attestation & submit. Nothing was submitted for you.', null, j.screenshot); return; }
+ // 2) fallback: openclaw not up -> paste card
+ fetch('/api/settlements/'+id+'/claim-and-go',{method:'POST'}).then(function(r){return r.json();}).then(function(k){
+ btn.disabled=false; btn.textContent='✅ Claim it & Go';
+ if(k.error){ alert(k.error); return; }
+ showPrep(name, k.note||'', k.fields, null);
+ var op=document.getElementById('prep-open'); if(k.mode_url){ op.href=k.mode_url; op.style.display=''; window.open(k.mode_url,'_blank','noopener'); }
+ });
}).catch(function(){ btn.disabled=false; btn.textContent='✅ Claim it & Go'; alert('error'); });
}; });
})();
← 61ad5f4 auto-data-snapshot: 2026-08-18T16:08:33 (8 data files) — dat
·
back to AbramsOS
·
claim-fill: positional last-name + Mode->official->form navi c5337bf →