Snapshot: full project state

This commit is contained in:
2026-10-06 23:43:48 -07:00
commit 4f56907c45
20 changed files with 1207 additions and 0 deletions

17
.gitignore vendored Normal file
View File

@@ -0,0 +1,17 @@
__pycache__/
*.pyc
node_modules/
.venv/
venv/
.env
*.db
*.sqlite*
*.log
.DS_Store
out/
work/
.pio/
briefs/
dns-backup/
archive/
*.png

44
README.md Normal file
View File

@@ -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.

302
app.py Normal file
View File

@@ -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)

290
autofill.py Normal file
View File

@@ -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 <select> options.
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",
}
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_profile():
with open(PROFILE_PATH) as fh:
return json.load(fh)
# JS: enumerate fillable fields (skip password/hidden/submit/etc).
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;
}
"""
# JS: fill one field. `candidates` = ordered values to try for <select>.
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()

15
autofill_profile.json Normal file
View File

@@ -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": ""
}

14
data/profile.json Normal file
View File

@@ -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": ""
}

69
data/requirements.json Normal file
View File

@@ -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"
]
}
}

1
data/secret.key Normal file
View File

@@ -0,0 +1 @@
iakl8iSPghLtG6yfgfSBZYhFtxPWdps9rt2wongG4zE=

19
fill_worker.py Normal file
View File

@@ -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)))

8
last_launch.json Normal file
View File

@@ -0,0 +1,8 @@
{
"launched": [
"Chase Credit Cards"
],
"urls": [
"https://creditcards.chase.com/"
]
}

24
providers.json Normal file
View File

@@ -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."}
]

2
requirements.txt Normal file
View File

@@ -0,0 +1,2 @@
flask>=2.0
cryptography>=3.4

106
static/style.css Normal file
View File

@@ -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); }

View File

@@ -0,0 +1,55 @@
{% extends "base.html" %}
{% block content %}
<div class="app-head">
<div>
<h1>{{ a['institution'] }}</h1>
<div class="muted">{{ a['account_type'] | replace('_', ' ') | title }} · started {{ a['created_at'] }}</div>
</div>
<form method="post" action="{{ url_for('set_status', app_id=a['id']) }}" class="inline">
<select name="status" onchange="this.form.submit()">
{% for s in ['researching', 'in_progress', 'submitted', 'approved', 'denied'] %}
<option value="{{ s }}" {% if a['status'] == s %}selected{% endif %}>{{ s }}</option>
{% endfor %}
</select>
</form>
</div>
<h2>Requirements</h2>
<ul class="checklist">
{% for item in items %}
<li>
<form method="post" action="{{ url_for('toggle_item', app_id=a['id'], item_id=item['id']) }}">
<button type="submit" class="checkbox {{ 'checked' if item['done'] else '' }}">
{{ '✓' if item['done'] else '' }}
</button>
<span class="{{ 'done' if item['done'] else '' }}">{{ item['label'] }}</span>
</form>
</li>
{% endfor %}
</ul>
<h2>Your info (for pre-filling this application)</h2>
{% if profile %}
<div class="profile-panel">
{% 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) %}
<div><span class="plabel">{{ label }}</span><span class="pvalue">{{ profile[key] }}</span></div>
{% endif %}
{% endfor %}
</div>
<p class="muted">Edit in <a href="{{ url_for('profile') }}">Profile</a>.</p>
{% else %}
<p class="muted">No profile yet. <a href="{{ url_for('profile') }}">Add your info</a> to pre-fill.</p>
{% endif %}
<h2>Notes</h2>
<form method="post" action="{{ url_for('set_notes', app_id=a['id']) }}" class="form">
<textarea name="notes" rows="3">{{ a['notes'] }}</textarea>
<button type="submit" class="btn">Save notes</button>
</form>
<form method="post" action="{{ url_for('delete_application', app_id=a['id']) }}"
onsubmit="return confirm('Delete this application?');">
<button type="submit" class="btn danger">Delete application</button>
</form>
{% endblock %}

41
templates/base.html Normal file
View File

@@ -0,0 +1,41 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow">
<title>Castor — Account Onboarding</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<header>
<div class="brand">
<span class="logo">✦</span>
<span>Castor</span>
<span class="tag">account onboarding assistant</span>
</div>
<nav>
<a href="{{ url_for('index') }}">Applications</a>
<a href="{{ url_for('new_application') }}">+ New</a>
<a href="{{ url_for('profile') }}">Profile</a>
<a href="{{ url_for('vault') }}">Vault</a>
</nav>
</header>
<main>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for cat, msg in messages %}
<div class="flash {{ cat }}">{{ msg }}</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</main>
<footer>
<p>For opening <strong>your own</strong> accounts in <strong>your own name</strong>. No identity fabrication, no bulk signups, no KYC evasion.</p>
</footer>
</body>
</html>

57
templates/index.html Normal file
View File

@@ -0,0 +1,57 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow">
<title>Castor — Signup Dashboard</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<header>
<div class="brand"><span class="logo">✦</span><span>Castor</span><span class="tag">signup dashboard</span></div>
<nav>
<a href="{{ url_for('index') }}">Providers</a>
<a href="{{ url_for('profile_route') }}">Your Info ({{ filled }}/{{ total }})</a>
</nav>
</header>
<main>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for cat, msg in messages %}<div class="flash {{ cat }}">{{ msg }}</div>{% endfor %}
{% endif %}
{% endwith %}
<div class="hero">
<h1>Open accounts without retyping your info</h1>
<p class="muted">Check the providers you want, hit launch — each signup page opens with your details already filled. You finish the CAPTCHA / verification and submit.</p>
</div>
<form method="post" action="{{ url_for('launch') }}">
{% for cat in categories %}
<h2 class="cat">{{ cat | replace('_', ' ') | title }}</h2>
<div class="cards">
{% for p in providers if p['category'] == cat %}
<label class="provider">
<input type="checkbox" name="providers" value="{{ p['id'] }}">
<span class="pname">{{ p['name'] }}</span>
<span class="pnote">{{ p['note'] }}</span>
<a class="plink" href="{{ p['url'] }}" target="_blank" rel="noopener" onclick="event.stopPropagation()">open ↗</a>
</label>
{% endfor %}
</div>
{% endfor %}
<div class="launchbar">
<button type="submit" class="btn primary big">Launch &amp; autofill selected</button>
<span class="muted">opens each in a new browser tab with your info filled</span>
</div>
</form>
</main>
<footer>
<p>Fills <strong>your own</strong> info into forms <strong>you</strong> submit. No identity fabrication, no bulk signups, no KYC evasion.</p>
</footer>
</body>
</html>

36
templates/new.html Normal file
View File

@@ -0,0 +1,36 @@
{% extends "base.html" %}
{% block content %}
<h1>New Application</h1>
<p class="muted">Pick the account type. Castor loads the real KYC requirements that institutions ask for.</p>
<form method="post" class="form">
<label>Account type
<select name="account_type" id="account_type">
{% for key, info in reqs.items() %}
<option value="{{ key }}">{{ info['label'] }}</option>
{% endfor %}
</select>
</label>
<div id="preview" class="preview"></div>
<label>Institution
<input type="text" name="institution" placeholder="e.g. Chase, Vanguard, Coinbase, PayPal" required>
</label>
<button type="submit" class="btn primary">Start application</button>
</form>
<script>
const reqs = {{ reqs | tojson }};
const sel = document.getElementById('account_type');
const preview = document.getElementById('preview');
function render() {
const info = reqs[sel.value];
preview.innerHTML = '<strong>Requirements:</strong><ul>' +
info.requirements.map(r => '<li>' + r + '</li>').join('') + '</ul>';
}
sel.addEventListener('change', render);
render();
</script>
{% endblock %}

42
templates/profile.html Normal file
View File

@@ -0,0 +1,42 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow">
<title>Castor — Your Info</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<header>
<div class="brand"><span class="logo">✦</span><span>Castor</span><span class="tag">signup dashboard</span></div>
<nav>
<a href="{{ url_for('index') }}">Providers</a>
<a href="{{ url_for('profile_route') }}">Your Info</a>
</nav>
</header>
<main>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for cat, msg in messages %}<div class="flash {{ cat }}">{{ msg }}</div>{% endfor %}
{% endif %}
{% endwith %}
<h1>Your Info</h1>
<p class="muted">Stored locally, used only to auto-fill <strong>your own</strong> signup forms.</p>
<form method="post" class="form">
{% for key, label in fields %}
<label>{{ label }}
<input type="text" name="{{ key }}" value="{{ profile.get(key, '') }}" autocomplete="off">
</label>
{% endfor %}
<button type="submit" class="btn primary">Save</button>
</form>
</main>
<footer>
<p>Fills <strong>your own</strong> info into forms <strong>you</strong> submit. No identity fabrication, no bulk signups, no KYC evasion.</p>
</footer>
</body>
</html>

37
templates/vault.html Normal file
View File

@@ -0,0 +1,37 @@
{% extends "base.html" %}
{% block content %}
<h1>Vault</h1>
<p class="muted">Your identity documents, encrypted at rest with a key generated on this machine.</p>
<form method="post" action="{{ url_for('vault_upload') }}" enctype="multipart/form-data" class="form">
<label>Upload document (20 MB max)
<input type="file" name="file" required>
</label>
<button type="submit" class="btn primary">Encrypt &amp; store</button>
</form>
{% if docs %}
<table class="table">
<thead><tr><th>File</th><th>Type</th><th>SHA-256</th><th>Uploaded</th><th></th></tr></thead>
<tbody>
{% for d in docs %}
<tr>
<td>{{ d['filename'] }}</td>
<td>{{ d['mime'] }}</td>
<td class="mono">{{ d['sha256'][:16] }}…</td>
<td>{{ d['uploaded_at'] }}</td>
<td class="row-actions">
<a class="btn small" href="{{ url_for('vault_download', doc_id=d['id']) }}">Download</a>
<form method="post" action="{{ url_for('vault_delete', doc_id=d['id']) }}" class="inline"
onsubmit="return confirm('Delete this document?');">
<button type="submit" class="btn small danger">Delete</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="muted">No documents stored yet.</p>
{% endif %}
{% endblock %}

28
test_form.html Normal file
View File

@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>Test Signup Form</title></head>
<body>
<h1>Open an Account</h1>
<form>
<label>First name <input type="text" name="firstname" autocomplete="given-name"></label><br>
<label>Last name <input type="text" name="lastname" autocomplete="family-name"></label><br>
<label>Email <input type="email" name="email" autocomplete="email"></label><br>
<label>Phone <input type="tel" name="phone" autocomplete="tel"></label><br>
<label>Street address <input type="text" name="address" autocomplete="street-address"></label><br>
<label>City <input type="text" name="city" autocomplete="address-level2"></label><br>
<label>State
<select name="state" autocomplete="address-level1">
<option value="">Select…</option>
<option value="CA">California</option>
<option value="WA">Washington</option>
<option value="OR">Oregon</option>
</select>
</label><br>
<label>ZIP <input type="text" name="zip" autocomplete="postal-code"></label><br>
<label>Date of birth <input type="text" name="dob" placeholder="MM/DD/YYYY"></label><br>
<label>Social Security Number <input type="text" name="ssn" autocomplete="off"></label><br>
<label>Password <input type="password" name="password"></label><br>
<button type="submit">Create Account</button>
</form>
</body>
</html>