From 4f56907c45fec295eb5173450baf8a9756bfb7a2 Mon Sep 17 00:00:00 2001 From: drjones Date: Tue, 6 Oct 2026 23:43:48 -0700 Subject: [PATCH] Snapshot: full project state --- .gitignore | 17 +++ README.md | 44 ++++++ app.py | 302 +++++++++++++++++++++++++++++++++++++ autofill.py | 290 +++++++++++++++++++++++++++++++++++ autofill_profile.json | 15 ++ data/profile.json | 14 ++ data/requirements.json | 69 +++++++++ data/secret.key | 1 + fill_worker.py | 19 +++ last_launch.json | 8 + providers.json | 24 +++ requirements.txt | 2 + static/style.css | 106 +++++++++++++ templates/application.html | 55 +++++++ templates/base.html | 41 +++++ templates/index.html | 57 +++++++ templates/new.html | 36 +++++ templates/profile.html | 42 ++++++ templates/vault.html | 37 +++++ test_form.html | 28 ++++ 20 files changed, 1207 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 app.py create mode 100644 autofill.py create mode 100644 autofill_profile.json create mode 100644 data/profile.json create mode 100644 data/requirements.json create mode 100644 data/secret.key create mode 100644 fill_worker.py create mode 100644 last_launch.json create mode 100644 providers.json create mode 100644 requirements.txt create mode 100644 static/style.css create mode 100644 templates/application.html create mode 100644 templates/base.html create mode 100644 templates/index.html create mode 100644 templates/new.html create mode 100644 templates/profile.html create mode 100644 templates/vault.html create mode 100644 test_form.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..24b2937 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +__pycache__/ +*.pyc +node_modules/ +.venv/ +venv/ +.env +*.db +*.sqlite* +*.log +.DS_Store +out/ +work/ +.pio/ +briefs/ +dns-backup/ +archive/ +*.png diff --git a/README.md b/README.md new file mode 100644 index 0000000..f70529e --- /dev/null +++ b/README.md @@ -0,0 +1,44 @@ +# Castor — Signup Dashboard + +Check the providers you want to open accounts with, hit **Launch & autofill**, and each +signup page opens in its own browser tab with your identity already filled. You finish the +CAPTCHA / SMS verification and submit. No retyping your info. + +## What it does +- **Provider catalog** (`providers.json`) — 22 financial providers across checking, savings, + brokerage, crypto, credit card, and payment apps, each with its direct signup link. +- **Checkbox selection** — pick the ones you want, launch them all at once. +- **Live autofill** — Playwright scans each form, matches fields (name, email, phone, address, + city, state, ZIP, DOB, SSN, employer, income) to your profile, and fills them with a highlight + flash while you watch. Password fields are never touched. +- **Profile** — your real info (`autofill_profile.json`), stored locally, used only to fill + *your own* forms. + +## Files +- `app.py` — Flask dashboard + the Playwright fill engine. +- `fill_worker.py` — subprocess entry the dashboard spawns to open the visible browser. +- `providers.json` — the provider catalog (name, category, signup URL, note). +- `autofill_profile.json` — your identity (fill in the blanks; SSN/DOB/address left empty for you). +- `test_form.html` — local test form for verifying the fill engine headless. + +## Run +```bash +cd ~/account-onboarding +/usr/bin/python3 app.py --host 0.0.0.0 --port 5057 +# open http://10.30.20.69:5057 +``` + +> **Use `/usr/bin/python3` explicitly.** In background/launched shells `python3` resolves to +> Homebrew Python 3.14, which does NOT have playwright installed. System Python 3.9 +> (`/usr/bin/python3`) has both `playwright` and `flask` in its user-site. + +## Test the fill engine without a browser window +```bash +/usr/bin/python3 fill_worker.py --urls '["file:///Users/drjones/account-onboarding/test_form.html"]' --headless +``` + +## Hard rules +- Single identity: one person, their real verified info, their own accounts. +- No identity fabrication, no bulk account creation, no KYC evasion. +- Fill-only by default; you review and submit. Real institutions still require CAPTCHA / + SMS / email verification, which is yours to complete. diff --git a/app.py b/app.py new file mode 100644 index 0000000..d6c35f9 --- /dev/null +++ b/app.py @@ -0,0 +1,302 @@ +#!/usr/bin/env python3 +""" +Castor — Signup Dashboard +Check the providers you want to open accounts with, then launch them in a +browser with your identity auto-filled. No retyping your info. + +It fills YOUR OWN verified info into forms YOU submit, one account at a time. +""" + +import argparse +import json +import os +import re +import subprocess +import sys + +from flask import Flask, request, render_template, redirect, url_for, flash + +from playwright.sync_api import sync_playwright + +BASE = os.path.dirname(os.path.abspath(__file__)) +PROFILE_PATH = os.path.join(BASE, "autofill_profile.json") +PROVIDERS_PATH = os.path.join(BASE, "providers.json") + +AUTOCOMPLETE_MAP = { + "given-name": "first_name", + "family-name": "last_name", + "name": "full_name", + "email": "email", + "tel": "phone", + "tel-national": "phone", + "tel-local": "phone", + "street-address": "address_line1", + "address-line1": "address_line1", + "address-line2": "address_line2", + "address-level1": "state", + "address-level2": "city", + "postal-code": "zip", + "bday": "dob", + "organization": "employer", + "organization-title": "employer", +} + +KEYWORD_MAP = { + "first_name": ["firstname", "givenname", "fname"], + "last_name": ["lastname", "surname", "familyname", "lname"], + "email": ["emailaddress", "email"], + "phone": ["phonenumber", "phonenum", "telephone", "mobile", "cellphone", "phone"], + "address_line1": ["streetaddress", "addressline1", "address1", "mailingaddress", "street"], + "address_line2": ["addressline2", "address2", "suite", "apt", "unit"], + "city": ["city", "town", "municipality"], + "state": ["stateprovince", "province", "state"], + "zip": ["zipcode", "postalcode", "zip", "postal"], + "dob": ["dateofbirth", "birthdate", "birthday", "dob"], + "ssn": ["socialsecuritynumber", "socialsecurity", "ssnnumber", "ssn", "taxpayerid", "taxid", "tin", "nationalid"], + "employer": ["currentemployer", "employername", "employer", "companyname"], + "income": ["annualincome", "incomeamount", "income", "salary"], +} + +STATE_NAMES = { + "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", "DC": "District of Columbia", +} + +PROFILE_FIELDS = [ + ("first_name", "First name"), ("last_name", "Last name"), ("email", "Email"), + ("phone", "Phone"), ("address_line1", "Address line 1"), ("address_line2", "Address line 2"), + ("city", "City"), ("state", "State (2-letter)"), ("zip", "ZIP"), + ("dob", "Date of birth (MM/DD/YYYY)"), ("ssn", "SSN (no dashes)"), + ("employer", "Employer"), ("income", "Annual income"), +] + +PASSWORD_LIKE = re.compile(r"password|passwd|passcode|pwd|confirmpassword", re.I) + + +def norm(s): + return re.sub(r"[^a-z0-9]", "", (s or "").lower()) + + +def load_json(path, default): + if os.path.exists(path): + with open(path) as fh: + return json.load(fh) + return default + + +def load_profile(): + return load_json(PROFILE_PATH, {}) + + +def save_profile(data): + with open(PROFILE_PATH, "w") as fh: + json.dump(data, fh, indent=2) + + +def load_providers(): + return load_json(PROVIDERS_PATH, []) + + +# ------------------------- Playwright fill engine ------------------------- +GATHER_FIELDS_JS = """ +() => { + const out = []; + const els = document.querySelectorAll('input, select, textarea'); + els.forEach((el, idx) => { + const tag = el.tagName.toLowerCase(); + const type = tag === 'input' ? (el.type || 'text').toLowerCase() : tag; + if (['hidden','submit','button','reset','checkbox','radio','file','password'].includes(type)) return; + if (el.offsetParent === null) return; + let label = ''; + if (el.id) { const l = document.querySelector('label[for="' + CSS.escape(el.id) + '"]'); if (l) label = l.innerText; } + if (!label && el.closest('label')) label = el.closest('label').innerText; + if (!label && el.parentElement) label = el.parentElement.innerText.split('\\n')[0]; + out.push({ idx, tag, type, name: el.name || '', id: el.id || '', ac: el.autocomplete || '', + ph: el.placeholder || '', al: el.getAttribute('aria-label') || '', label: (label || '').trim() }); + }); + return out; +} +""" + +FILL_ONE_JS = """ +(arg) => { + const idx = arg.idx; const candidates = arg.candidates; + const els = document.querySelectorAll('input, select, textarea'); + const el = els[idx]; + if (!el) return 'missing'; + el.scrollIntoView({ block: 'center' }); + el.style.outline = '3px solid #6c8cff'; el.style.outlineOffset = '1px'; + let ok = false; + if (el.tagName === 'SELECT') { + const opts = Array.from(el.options); + outer: + for (const want of candidates) { + const w = String(want).toLowerCase(); + for (const o of opts) { + const t = o.text.trim().toLowerCase(); + if (t === w || t.includes(w) || w.includes(t) || o.value.toLowerCase() === w) { el.value = o.value; ok = true; break outer; } + } + } + } else { + const proto = el.tagName === 'TEXTAREA' ? window.HTMLTextAreaElement.prototype : window.HTMLInputElement.prototype; + Object.getOwnPropertyDescriptor(proto, 'value').set.call(el, String(candidates[0])); + ok = true; + } + el.dispatchEvent(new Event('input', { bubbles: true })); + el.dispatchEvent(new Event('change', { bubbles: true })); + setTimeout(() => { el.style.outline = ok ? '2px solid #4ade80' : '2px solid #f87171'; }, 500); + return ok ? 'filled' : 'no-match'; +} +""" + + +def build_sig(f): + return norm(" ".join([f["name"], f["id"], f["ph"], f["al"], f["label"]])) + + +def match_profile_key(f): + ac = f["ac"].strip().lower() + if ac in AUTOCOMPLETE_MAP: + return AUTOCOMPLETE_MAP[ac] + sig = build_sig(f) + best_key, best_len = None, 0 + for key, kws in KEYWORD_MAP.items(): + for kw in kws: + if kw in sig and len(kw) > best_len: + best_key, best_len = key, len(kw) + return best_key + + +def fill_candidates(profile, key): + val = profile.get(key, "") + if not val: + return None + cands = [val] + if key == "state": + cands.append(STATE_NAMES.get(val.upper(), val)) + if key == "full_name": + cands = ["%s %s" % (profile.get("first_name", ""), profile.get("last_name", "")).strip()] + return cands + + +def fill_page(page, profile, delay=0.2): + fields = page.evaluate(GATHER_FIELDS_JS) + results = [] + for f in fields: + blob = f["name"] + " " + f["id"] + " " + f["label"] + " " + f["ac"] + if PASSWORD_LIKE.search(blob): + continue + key = match_profile_key(f) + if key is None: + continue + cands = fill_candidates(profile, key) + if not cands: + continue + page.evaluate(FILL_ONE_JS, {"idx": f["idx"], "candidates": cands}) + results.append((key, f, cands[0])) + if delay: + page.wait_for_timeout(int(delay * 1000)) + return results + + +def launch_fill(urls, headless=False, delay=0.2): + """Open each URL in its own tab, auto-fill, leave the browser open. Returns a summary.""" + import time + profile = load_profile() + summary = [] + with sync_playwright() as p: + browser = p.chromium.launch(headless=headless) + context = browser.new_context() + pages = [] + for url in urls: + page = context.new_page() + pages.append(page) + page.goto(url, wait_until="domcontentloaded", timeout=60000) + page.wait_for_timeout(1500) + results = fill_page(page, profile, delay) + summary.append((url, results)) + if not headless: + # Keep the browser alive until the user closes it (signups in progress). + while browser.is_connected(): + time.sleep(1) + browser.close() + return summary + + +# ------------------------- Flask app ------------------------- +app = Flask(__name__) +app.secret_key = "castor-signup-dashboard-local-only" + + +@app.route("/") +def index(): + providers = load_providers() + profile = load_profile() + filled = sum(1 for k, v in profile.items() if v) + total = len(PROFILE_FIELDS) + categories = [] + seen = [] + for p in providers: + c = p["category"] + if c not in seen: + seen.append(c) + categories.append(c) + return render_template( + "index.html", providers=providers, categories=categories, + profile=profile, filled=filled, total=total, + ) + + +@app.route("/launch", methods=["POST"]) +def launch(): + ids = request.form.getlist("providers") + providers = load_providers() + chosen = [p for p in providers if p["id"] in ids] + if not chosen: + flash("Select at least one provider.", "error") + return redirect(url_for("index")) + urls = [p["url"] for p in chosen] + names = [p["name"] for p in chosen] + + # Fill a report log to disk, then launch the fill engine in a visible browser. + report = {"launched": names, "urls": urls} + with open(os.path.join(BASE, "last_launch.json"), "w") as fh: + json.dump(report, fh, indent=2) + + # Launch in a subprocess so the visible browser opens on this machine. + subprocess.Popen( + [sys.executable, os.path.join(BASE, "fill_worker.py"), "--urls", json.dumps(urls)], + cwd=BASE, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, + ) + + flash("Opened %d signup page(s) — completing auto-fill in the browser now." % len(chosen), "ok") + return redirect(url_for("index")) + + +@app.route("/profile", methods=["GET", "POST"]) +def profile_route(): + if request.method == "POST": + data = {} + for key, _label in PROFILE_FIELDS: + data[key] = request.form.get(key, "").strip() + save_profile(data) + flash("Profile saved.", "ok") + return redirect(url_for("profile_route")) + return render_template("profile.html", fields=PROFILE_FIELDS, profile=load_profile()) + + +if __name__ == "__main__": + ap = argparse.ArgumentParser() + ap.add_argument("--host", default="127.0.0.1") + ap.add_argument("--port", type=int, default=5057) + args = ap.parse_args() + app.run(host=args.host, port=args.port, debug=False) diff --git a/autofill.py b/autofill.py new file mode 100644 index 0000000..8500b5c --- /dev/null +++ b/autofill.py @@ -0,0 +1,290 @@ +#!/usr/bin/env python3 +""" +autofill.py — real-time account signup form filler. + +Opens a signup page, scans every visible form field, matches it against your +identity profile, and fills each matched field live (highlighted, one at a +time) while you watch. It NEVER auto-submits by default — you review and hit +submit yourself. + +Usage: + python3 autofill.py --url https://example.com/signup + python3 autofill.py --url https://example.com/signup --headless --dump # test mode + python3 autofill.py --url https://example.com/signup --submit # fill + click submit + +Profile: autofill_profile.json (your real info; you fill in the blanks). +""" + +import argparse +import json +import os +import re + +from playwright.sync_api import sync_playwright + +BASE = os.path.dirname(os.path.abspath(__file__)) +PROFILE_PATH = os.path.join(BASE, "autofill_profile.json") + +# Standardized HTML autocomplete attribute -> profile key (highest confidence). +AUTOCOMPLETE_MAP = { + "given-name": "first_name", + "family-name": "last_name", + "name": "full_name", + "email": "email", + "tel": "phone", + "tel-national": "phone", + "tel-local": "phone", + "street-address": "address_line1", + "address-line1": "address_line1", + "address-line2": "address_line2", + "address-level1": "state", + "address-level2": "city", + "postal-code": "zip", + "bday": "dob", + "organization": "employer", + "organization-title": "employer", +} + +# Fallback keyword -> profile key, matched against name/id/label/placeholder. +# Deliberately avoids greedy tokens like bare "address", "first", "last". +KEYWORD_MAP = { + "first_name": ["firstname", "givenname", "fname"], + "last_name": ["lastname", "surname", "familyname", "lname"], + "email": ["emailaddress", "email"], + "phone": ["phonenumber", "phonenum", "telephone", "mobile", "cellphone", "phone"], + "address_line1": ["streetaddress", "addressline1", "address1", "mailingaddress", "street"], + "address_line2": ["addressline2", "address2", "suite", "apt", "unit"], + "city": ["city", "town", "municipality"], + "state": ["stateprovince", "province", "state"], + "zip": ["zipcode", "postalcode", "zip", "postal"], + "dob": ["dateofbirth", "birthdate", "birthday", "dob"], + "ssn": [ + "socialsecuritynumber", "socialsecurity", "ssnnumber", "ssn", + "taxpayerid", "taxid", "tin", "nationalid", + ], + "employer": ["currentemployer", "employername", "employer", "companyname"], + "income": ["annualincome", "incomeamount", "income", "salary"], +} + +# US state abbreviation -> full name, for matching . +FILL_ONE_JS = """ +(arg) => { + const idx = arg.idx; + const candidates = arg.candidates; + const els = document.querySelectorAll('input, select, textarea'); + const el = els[idx]; + if (!el) return 'missing'; + el.scrollIntoView({ block: 'center' }); + el.style.outline = '3px solid #6c8cff'; + el.style.outlineOffset = '1px'; + let ok = false; + if (el.tagName === 'SELECT') { + const opts = Array.from(el.options); + outer: + for (const want of candidates) { + const w = String(want).toLowerCase(); + for (const o of opts) { + const t = o.text.trim().toLowerCase(); + if (t === w || t.includes(w) || w.includes(t) || o.value.toLowerCase() === w) { + el.value = o.value; ok = true; break outer; + } + } + } + } else { + const proto = el.tagName === 'TEXTAREA' + ? window.HTMLTextAreaElement.prototype + : window.HTMLInputElement.prototype; + const setter = Object.getOwnPropertyDescriptor(proto, 'value').set; + setter.call(el, String(candidates[0])); + ok = true; + } + el.dispatchEvent(new Event('input', { bubbles: true })); + el.dispatchEvent(new Event('change', { bubbles: true })); + setTimeout(() => { el.style.outline = ok ? '2px solid #4ade80' : '2px solid #f87171'; }, 500); + return ok ? 'filled' : 'no-match'; +} +""" + +READ_BACK_JS = """ +(arg) => { + const els = document.querySelectorAll('input, select, textarea'); + const el = els[arg.idx]; + return el ? el.value : null; +} +""" + + +def build_sig(f): + return norm(" ".join([f["name"], f["id"], f["ph"], f["al"], f["label"]])) + + +def match_profile_key(f): + # 1) autocomplete attribute is authoritative. + ac = f["ac"].strip().lower() + if ac in AUTOCOMPLETE_MAP: + return AUTOCOMPLETE_MAP[ac] + # 2) fall back to keyword matching on name/id/label/placeholder. + sig = build_sig(f) + best_key, best_len = None, 0 + for key, kws in KEYWORD_MAP.items(): + for kw in kws: + if kw in sig: + if len(kw) > best_len: + best_key, best_len = key, len(kw) + return best_key + + +def fill_candidates(profile, key, f): + val = profile.get(key, "") + if not val: + return None + cands = [val] + if key == "state": + cands.append(STATE_NAMES.get(val.upper(), val)) + if key == "full_name": + first = profile.get("first_name", "") + last = profile.get("last_name", "") + cands = ["%s %s" % (first, last).strip()] + return cands + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--url", required=True) + ap.add_argument("--headless", action="store_true") + ap.add_argument("--dump", action="store_true", help="fill then print what was set (test mode)") + ap.add_argument("--submit", action="store_true", help="click the first submit button after filling") + ap.add_argument("--delay", type=float, default=0.35, help="seconds between field fills") + args = ap.parse_args() + + profile = load_profile() + + with sync_playwright() as p: + browser = p.chromium.launch(headless=args.headless) + page = browser.new_page() + page.goto(args.url, wait_until="domcontentloaded", timeout=60000) + page.wait_for_timeout(1500) + + fields = page.evaluate(GATHER_FIELDS_JS) + results = [] + + for f in fields: + blob = f["name"] + " " + f["id"] + " " + f["label"] + " " + f["ac"] + if PASSWORD_LIKE.search(blob): + continue + key = match_profile_key(f) + if key is None: + continue + cands = fill_candidates(profile, key, f) + if not cands: + continue + page.evaluate(FILL_ONE_JS, {"idx": f["idx"], "candidates": cands}) + results.append((key, f, cands[0])) + if args.delay: + page.wait_for_timeout(int(args.delay * 1000)) + + print("\n=== FILLED %d FIELDS ===" % len(results)) + for key, f, val in results: + shown = "***-**-****" if key == "ssn" else val + tag = f["label"] or f["name"] or f["id"] or f["ph"] + print(f" {key:<14} <- {shown!r:>24} [{tag}]") + + if args.dump: + print("\n=== READ-BACK VERIFY ===") + all_ok = True + for key, f, val in results: + actual = page.evaluate(READ_BACK_JS, {"idx": f["idx"]}) + ok = actual == val + all_ok = all_ok and ok + print(f" {key:<14} {'OK' if ok else 'MISMATCH':>8} set={val!r} got={actual!r}") + print("\nRESULT:", "ALL MATCH" if all_ok else "FAILURES PRESENT") + + if args.submit: + clicked = page.evaluate(""" + () => { + const sels = [ + 'button[type="submit"]', 'input[type="submit"]', + 'button:has-text("Continue")', 'button:has-text("Next")', + 'button:has-text("Submit")', 'button:has-text("Sign up")', + 'button:has-text("Create account")', 'button:has-text("Agree")' + ]; + for (const s of sels) { + const el = document.querySelector(s); + if (el && el.offsetParent !== null) { el.click(); return s; } + } + return null; + } + """) + print("\n[SUBMIT] clicked:", clicked) + + if not args.headless and not args.dump: + print("\nBrowser left open — review the form and submit yourself.") + input("Press Enter in this terminal to close the browser...") + + browser.close() + + +if __name__ == "__main__": + main() diff --git a/autofill_profile.json b/autofill_profile.json new file mode 100644 index 0000000..73937e3 --- /dev/null +++ b/autofill_profile.json @@ -0,0 +1,15 @@ +{ + "first_name": "Indiana", + "last_name": "Holmes", + "email": "indianaholmes1@icloud.com", + "phone": "", + "address_line1": "", + "address_line2": "", + "city": "Lynnwood", + "state": "WA", + "zip": "", + "dob": "", + "ssn": "", + "employer": "", + "income": "" +} diff --git a/data/profile.json b/data/profile.json new file mode 100644 index 0000000..85e0577 --- /dev/null +++ b/data/profile.json @@ -0,0 +1,14 @@ +{ + "full_name": "Indiana Holmes", + "date_of_birth": "", + "ssn": "", + "address_line1": "", + "address_line2": "", + "city": "Lynnwood", + "state": "WA", + "zip": "", + "phone": "", + "email": "indianaholmes1@icloud.com", + "employer": "", + "income": "" +} \ No newline at end of file diff --git a/data/requirements.json b/data/requirements.json new file mode 100644 index 0000000..040c895 --- /dev/null +++ b/data/requirements.json @@ -0,0 +1,69 @@ +{ + "checking": { + "label": "Checking Account", + "requirements": [ + "Government-issued photo ID (driver's license or passport)", + "Social Security Number / Taxpayer ID", + "Date of birth", + "Proof of address (utility bill, lease, or bank statement)", + "Opening deposit (amount varies by institution)", + "Phone number and email address" + ] + }, + "savings": { + "label": "Savings Account", + "requirements": [ + "Government-issued photo ID", + "Social Security Number / Taxpayer ID", + "Date of birth", + "Proof of address", + "Opening deposit (varies; many are $0 minimum)", + "Phone number and email address" + ] + }, + "brokerage": { + "label": "Brokerage / Investment Account", + "requirements": [ + "Government-issued photo ID", + "Social Security Number / Taxpayer ID", + "Employment status and (sometimes) employer info", + "Funding source (linked bank routing + account number)", + "Risk / suitability questionnaire", + "Phone number and email address" + ] + }, + "crypto_exchange": { + "label": "Cryptocurrency Exchange", + "requirements": [ + "Government-issued photo ID", + "Social Security Number / Taxpayer ID", + "Liveness selfie check (some exchanges)", + "Proof of address", + "Funding source (bank link or card)", + "Two-factor authentication enabled", + "Phone number and email address" + ] + }, + "credit_card": { + "label": "Credit Card", + "requirements": [ + "Government-issued photo ID", + "Social Security Number / Taxpayer ID", + "Annual income", + "Employment status and employer", + "Monthly housing payment", + "Consent to a credit check (hard pull)", + "Phone number and email address" + ] + }, + "payment_app": { + "label": "Payment App (PayPal / Cash App / Venmo)", + "requirements": [ + "Phone number and email address", + "Government-issued photo ID (for higher limits)", + "Social Security Number / Taxpayer ID (for higher limits)", + "Linked bank account or debit card", + "Date of birth" + ] + } +} \ No newline at end of file diff --git a/data/secret.key b/data/secret.key new file mode 100644 index 0000000..a1e679c --- /dev/null +++ b/data/secret.key @@ -0,0 +1 @@ +iakl8iSPghLtG6yfgfSBZYhFtxPWdps9rt2wongG4zE= \ No newline at end of file diff --git a/fill_worker.py b/fill_worker.py new file mode 100644 index 0000000..f9ac1d3 --- /dev/null +++ b/fill_worker.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +"""Worker entry point launched by the Castor dashboard to run the visible +autofill browser as a subprocess. Import the engine from app.py.""" +import argparse +import json +import sys + +from app import launch_fill + +if __name__ == "__main__": + ap = argparse.ArgumentParser() + ap.add_argument("--urls", required=True, help="JSON array of signup URLs") + ap.add_argument("--headless", action="store_true") + args = ap.parse_args() + + urls = json.loads(args.urls) + summary = launch_fill(urls, headless=args.headless) + for url, results in summary: + print("%s -> filled %d fields" % (url, len(results))) diff --git a/last_launch.json b/last_launch.json new file mode 100644 index 0000000..62cd7d6 --- /dev/null +++ b/last_launch.json @@ -0,0 +1,8 @@ +{ + "launched": [ + "Chase Credit Cards" + ], + "urls": [ + "https://creditcards.chase.com/" + ] +} \ No newline at end of file diff --git a/providers.json b/providers.json new file mode 100644 index 0000000..180b83a --- /dev/null +++ b/providers.json @@ -0,0 +1,24 @@ +[ + {"id": "chase_checking", "name": "Chase Total Checking", "category": "checking", "url": "https://account.chase.com/consumer/banking/enroll", "note": "Big-bank checking; nationwide branches."}, + {"id": "chase_savings", "name": "Chase Savings", "category": "savings", "url": "https://account.chase.com/consumer/banking/enroll", "note": "Standard savings account."}, + {"id": "capital_one_360", "name": "Capital One 360 Checking", "category": "checking", "url": "https://www.capitalone.com/bank/checking-accounts/", "note": "No-fee online checking."}, + {"id": "discover_bank", "name": "Discover Online Banking", "category": "checking", "url": "https://www.discover.com/online-banking/", "note": "Cashback checking + savings."}, + {"id": "ally", "name": "Ally Bank", "category": "savings", "url": "https://www.ally.com/bank/", "note": "Online-only, high-yield savings."}, + {"id": "chime", "name": "Chime", "category": "checking", "url": "https://www.chime.com/enroll/", "note": "Fee-free mobile banking."}, + {"id": "sofi", "name": "SoFi Banking", "category": "checking", "url": "https://www.sofi.com/banking/", "note": "High-yield checking + savings."}, + {"id": "robinhood", "name": "Robinhood", "category": "brokerage", "url": "https://robinhood.com/us/en/", "note": "Commission-free stock/crypto trading."}, + {"id": "fidelity", "name": "Fidelity", "category": "brokerage", "url": "https://www.fidelity.com/open-account/overview", "note": "Full-service brokerage."}, + {"id": "schwab", "name": "Charles Schwab", "category": "brokerage", "url": "https://www.schwab.com/open-an-account", "note": "Brokerage + checking."}, + {"id": "vanguard", "name": "Vanguard", "category": "brokerage", "url": "https://investor.vanguard.com/", "note": "Low-cost index investing."}, + {"id": "webull", "name": "Webull", "category": "brokerage", "url": "https://www.webull.com/register", "note": "Commission-free trading app."}, + {"id": "coinbase", "name": "Coinbase", "category": "crypto", "url": "https://www.coinbase.com/signup", "note": "Largest US crypto exchange."}, + {"id": "kraken", "name": "Kraken", "category": "crypto", "url": "https://www.kraken.com/signup", "note": "Crypto exchange, strong security."}, + {"id": "gemini", "name": "Gemini", "category": "crypto", "url": "https://exchange.gemini.com/register", "note": "Regulated US crypto exchange."}, + {"id": "crypto_com", "name": "Crypto.com", "category": "crypto", "url": "https://crypto.com/exchange/signup", "note": "Crypto exchange + card."}, + {"id": "chase_cards", "name": "Chase Credit Cards", "category": "credit_card", "url": "https://creditcards.chase.com/", "note": "Rewards and travel cards."}, + {"id": "amex", "name": "American Express", "category": "credit_card", "url": "https://www.americanexpress.com/us/credit-cards/", "note": "Premium rewards cards."}, + {"id": "capital_one_cards", "name": "Capital One Cards", "category": "credit_card", "url": "https://www.capitalone.com/credit-cards/", "note": "Cashback and travel cards."}, + {"id": "paypal", "name": "PayPal", "category": "payment", "url": "https://www.paypal.com/us/webapps/mpp/account-selection", "note": "Digital wallet and payments."}, + {"id": "venmo", "name": "Venmo", "category": "payment", "url": "https://venmo.com/signup", "note": "P2P payments app."}, + {"id": "cash_app", "name": "Cash App", "category": "payment", "url": "https://cash.app/", "note": "P2P + stock/bitcoin app."} +] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..746ea83 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +flask>=2.0 +cryptography>=3.4 diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..a5b91e9 --- /dev/null +++ b/static/style.css @@ -0,0 +1,106 @@ +:root { + --bg: #0b1020; + --panel: #141b30; + --panel-2: #1b2340; + --text: #e7ecf5; + --muted: #8a93ab; + --accent: #6c8cff; + --accent-2: #9a6cff; + --ok: #4ade80; + --err: #f87171; + --border: #2a3352; +} + +* { box-sizing: border-box; } + +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + background: radial-gradient(circle at 20% 0%, #1a2140 0%, var(--bg) 60%); + color: var(--text); + min-height: 100vh; + display: flex; + flex-direction: column; +} + +header { + display: flex; align-items: center; justify-content: space-between; + padding: 14px 24px; + background: rgba(20, 27, 48, 0.7); + border-bottom: 1px solid var(--border); + backdrop-filter: blur(6px); + position: sticky; top: 0; z-index: 10; +} + +.brand { display: flex; align-items: baseline; gap: 8px; font-weight: 700; font-size: 1.15rem; } +.logo { color: var(--accent-2); } +.tag { color: var(--muted); font-weight: 400; font-size: 0.85rem; } + +nav { display: flex; gap: 18px; } +nav a { color: var(--muted); text-decoration: none; font-weight: 500; } +nav a:hover { color: var(--text); } + +main { flex: 1; max-width: 900px; width: 100%; margin: 0 auto; padding: 28px 24px; } + +footer { + padding: 16px 24px; text-align: center; color: var(--muted); + font-size: 0.82rem; border-top: 1px solid var(--border); +} + +h1 { margin: 0 0 12px; font-size: 1.6rem; } +h2.cat { margin: 26px 0 12px; font-size: 1.05rem; color: var(--accent); text-transform: uppercase; letter-spacing: 0.04em; } +.muted { color: var(--muted); } + +.hero { margin-bottom: 8px; } +.hero p { max-width: 620px; line-height: 1.5; } + +.flash { padding: 10px 14px; border-radius: 8px; margin-bottom: 16px; font-weight: 500; } +.flash.ok { background: rgba(74, 222, 128, 0.12); color: var(--ok); } +.flash.error { background: rgba(248, 113, 113, 0.12); color: var(--err); } + +.btn { + display: inline-block; + background: var(--panel-2); color: var(--text); + border: 1px solid var(--border); + padding: 9px 16px; border-radius: 8px; cursor: pointer; + text-decoration: none; font-size: 0.92rem; font-weight: 600; +} +.btn:hover { border-color: var(--accent); } +.btn.primary { background: linear-gradient(135deg, var(--accent), var(--accent-2)); border: none; } +.btn.big { padding: 13px 26px; font-size: 1.02rem; } + +.cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 12px; } + +.provider { + display: flex; flex-direction: column; gap: 4px; + background: var(--panel); border: 1px solid var(--border); + border-radius: 10px; padding: 14px 16px; cursor: pointer; + transition: border-color 0.15s, transform 0.1s; +} +.provider:hover { border-color: var(--accent); } +.provider:has(input:checked) { border-color: var(--accent); background: var(--panel-2); box-shadow: 0 0 0 1px var(--accent); } + +.provider input[type="checkbox"] { + position: absolute; opacity: 0; pointer-events: none; +} + +.pname { font-weight: 700; font-size: 1rem; } +.pnote { color: var(--muted); font-size: 0.8rem; line-height: 1.35; } +.plink { color: var(--accent-2); font-size: 0.8rem; text-decoration: none; align-self: flex-start; margin-top: 2px; } +.plink:hover { text-decoration: underline; } + +.launchbar { + display: flex; align-items: center; gap: 14px; + margin-top: 28px; padding: 16px; + background: var(--panel); border: 1px solid var(--border); border-radius: 12px; + position: sticky; bottom: 16px; +} +.launchbar .muted { font-size: 0.85rem; } + +.form { display: flex; flex-direction: column; gap: 16px; max-width: 480px; } +.form label { display: flex; flex-direction: column; gap: 6px; font-weight: 600; font-size: 0.9rem; } +.form input { + background: var(--panel-2); border: 1px solid var(--border); color: var(--text); + padding: 10px 12px; border-radius: 8px; font-size: 0.95rem; +} +.form input:focus { outline: none; border-color: var(--accent); } diff --git a/templates/application.html b/templates/application.html new file mode 100644 index 0000000..bbbcab9 --- /dev/null +++ b/templates/application.html @@ -0,0 +1,55 @@ +{% extends "base.html" %} +{% block content %} +
+
+

{{ a['institution'] }}

+
{{ a['account_type'] | replace('_', ' ') | title }} · started {{ a['created_at'] }}
+
+
+ +
+
+ +

Requirements

+ + +

Your info (for pre-filling this application)

+{% if profile %} +
+ {% for key, label in [('full_name','Name'),('date_of_birth','DOB'),('ssn','SSN/TIN'),('address_line1','Address'),('city','City'),('state','State'),('zip','ZIP'),('phone','Phone'),('email','Email')] %} + {% if profile.get(key) %} +
{{ label }}{{ profile[key] }}
+ {% endif %} + {% endfor %} +
+

Edit in Profile.

+{% else %} +

No profile yet. Add your info to pre-fill.

+{% endif %} + +

Notes

+
+ + +
+ +
+ +
+{% endblock %} diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..c973cd6 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,41 @@ + + + + + + + Castor — Account Onboarding + + + +
+
+ + Castor + account onboarding assistant +
+ +
+ +
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for cat, msg in messages %} +
{{ msg }}
+ {% endfor %} + {% endif %} + {% endwith %} + + {% block content %}{% endblock %} +
+ + + + diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..74e173f --- /dev/null +++ b/templates/index.html @@ -0,0 +1,57 @@ + + + + + + + Castor — Signup Dashboard + + + +
+
Castorsignup dashboard
+ +
+ +
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for cat, msg in messages %}
{{ msg }}
{% endfor %} + {% endif %} + {% endwith %} + +
+

Open accounts without retyping your info

+

Check the providers you want, hit launch — each signup page opens with your details already filled. You finish the CAPTCHA / verification and submit.

+
+ +
+ {% for cat in categories %} +

{{ cat | replace('_', ' ') | title }}

+
+ {% for p in providers if p['category'] == cat %} + + {% endfor %} +
+ {% endfor %} + +
+ + opens each in a new browser tab with your info filled +
+
+
+ + + + diff --git a/templates/new.html b/templates/new.html new file mode 100644 index 0000000..cf08010 --- /dev/null +++ b/templates/new.html @@ -0,0 +1,36 @@ +{% extends "base.html" %} +{% block content %} +

New Application

+

Pick the account type. Castor loads the real KYC requirements that institutions ask for.

+ +
+ + +
+ + + + +
+ + +{% endblock %} diff --git a/templates/profile.html b/templates/profile.html new file mode 100644 index 0000000..aca1c7c --- /dev/null +++ b/templates/profile.html @@ -0,0 +1,42 @@ + + + + + + + Castor — Your Info + + + +
+
Castorsignup dashboard
+ +
+ +
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for cat, msg in messages %}
{{ msg }}
{% endfor %} + {% endif %} + {% endwith %} + +

Your Info

+

Stored locally, used only to auto-fill your own signup forms.

+
+ {% for key, label in fields %} + + {% endfor %} + +
+
+ + + + diff --git a/templates/vault.html b/templates/vault.html new file mode 100644 index 0000000..2ed4fcd --- /dev/null +++ b/templates/vault.html @@ -0,0 +1,37 @@ +{% extends "base.html" %} +{% block content %} +

Vault

+

Your identity documents, encrypted at rest with a key generated on this machine.

+ +
+ + +
+ +{% if docs %} + + + + {% for d in docs %} + + + + + + + + {% endfor %} + +
FileTypeSHA-256Uploaded
{{ d['filename'] }}{{ d['mime'] }}{{ d['sha256'][:16] }}…{{ d['uploaded_at'] }} + Download +
+ +
+
+{% else %} +

No documents stored yet.

+{% endif %} +{% endblock %} diff --git a/test_form.html b/test_form.html new file mode 100644 index 0000000..6b0879c --- /dev/null +++ b/test_form.html @@ -0,0 +1,28 @@ + + +Test Signup Form + +

Open an Account

+
+
+
+
+
+
+
+
+
+
+
+
+ +
+ +