[object Object]

← 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

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{'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[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 &amp; 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 →