| Item | Amount | Date / Freq | Notes |
|---|---|---|---|
| One-Time Costs | |||
| California LLC Filing | $75 | Mar 26, 2026 | Filed with CA Secretary of State |
| Anthropic API Credits (Claude tokens) | $200 | Mar 2026 | 2x $100 top-ups — powers Lobster voice, chat, reports, Claude Code |
| Spent to date | $275 | ||
| Monthly Recurring (Active) | |||
| Claude Max | $100/mo | Monthly | claude.ai + Claude Code usage (flat rate) |
| Anthropic API Credits | pay-as-you-go | Monthly | Separate from Claude Max — Lobster voice, chat, reports endpoints |
| Apify Starter Plan | $29/mo | Monthly | CL scraping credits + platform access |
| ivanvs CL Scraper Actor | $25/mo | Monthly | Rented actor for Craigslist vehicle scraping |
| Twilio Phone Number | ~$1/mo | Monthly | +15106163508 — voice + SMS |
| Railway Hosting | ~$5/mo | Monthly | Usage-based — always-on server + auto-deploy |
| Monthly subtotal | ~$160/mo | ||
| Pending / Upcoming | |||
| CA LLC Annual Franchise Tax | $800/yr | Due 2027 | California minimum franchise tax |
| EIN (IRS) | $0 | Pending | Free — apply at irs.gov after LLC approved |
| Twilio Standard Brand | $4/mo | After LLC | Required for A2P 10DLC SMS compliance |
| Apify Scrape Credits | ~$15/mo | Estimated | 2x daily CL runs across 5 regions |
| Projected after LLC | ~$179/mo | + $800/yr franchise tax | |
127.0.0.1:9222), navigates to app.vettx.com/fresh, scans card tiles for vehicle dataextractTitle() parses year/make/model from tile text, extractPrice() / extractMileage() / extractVin() pull structured fieldsvehicle.title, vehicle.vin, vehicle.mileage, vehicle.photos[], seller.askingPrice, _listingUrl, _channelActionclaimedLeads Set tracks fingerprints (price|mileage), VINs, and IDs across restartsstate.done = true)npm start → node src/run.js → startWorker()fb-actor-input.json — queries: "cars for sale", "car for sale private", radius: 150mi from SF Bay Area, $1k–$30k, max 200 items, category: vehiclesnormalizeFBLead(item, datasetId) in facebookMarketplace.jsid (fb-timestamp-rand), source: "apify_fb", channel: "facebook", vehicle.{title, year, make, model, mileage, vin, photos[]}, seller.{name, askingPrice, contactInfo.phone}item.images[] already populated from Apify — no lobster neededPOST /api/inbound/facebook exists in server.js (line 1472) but actor is not scheduled yet/api/inbound/facebook → leads flow automaticallynormalizeApifyLead(item, source, datasetId)id (apify-timestamp-rand), source: "apify", channel: "craigslist", full vehicle + seller schema_50x50c.jpg → _1200x900.jpg, deduplicates by image ID hash, strips remaining thumbnailsisDealerPost() checks description for 3+ dealer signals (dealer, auto sales, financing available, etc.)POST /api/inbound/listing (line 1374)POST /api/admin/run-scraper triggers Apify actor via APIPOST /api/inbound/listing (CL/Apify), POST /api/inbound/facebook (FB/Apify), saveLead() from workerPOST /api/leads requires Bearer tokentitle, missing fields flagged in _needsReview / _missingFromScraper[]findLeadByListingUrl(url) prevents duplicate ingest of same listing!url.includes("50x50") && !url.includes("_50x") && !url.includes("50c.jpg")_ingestOrder: Date.now() for reliable sort (replaces claimedAt ordering)id — unique string (vettx-*, apify-*, fb-*)status — pending_evaluation → evaluating → captured_valuation → offer_approved / rejected / stale / archivedsource — "vettx" | "apify" | "apify_fb"channel — "craigslist" | "facebook"vehicle.{title, vin, year, make, model, mileage, condition, photos[], primaryPhoto, reconNotes}seller.{name, askingPrice, contactInfo.phone, motivation}_listingUrl — original listing URL (NOT lead.url)_expired — true if listing gone (404/410)_vAutoAppraisal — captured vAuto data (NEVER delete leads with this)_vAutoSubmitted — true if sent to vAutoconversation.{messages[], summary, keyFacts, lastMessageAt, messageCount}evaluation.{vAutoResult, suggestedOffer, maxOffer, approved, approvedAt}fetchLeads() — GET /api/leads, filter !vehicle.photos.length && _listingUrl && !_expired, sort FB first (URLs expire), then newestleadNeedsPhotos(leadId) — re-checks live state before opening browser to avoid raceschromium.launchPersistentContext() with copied Chrome cookies (avoids SingletonLock). Temp profile in /tmp/photo-lobster-*scrapeCraigslist(page) — grabs images.craigslist.org URLs, upgrades via clFullSize() to _1200x900.jpg/\d+_\d+\.jpg/ or containing _1200x900 — rejects icon sprites that share the CL domainscrapeFacebook(page) — collects fbcdn.net/scontent imgs, dedupes by fbBaseKey(), keeps largest area, upgrades to _o.jpgscrapeGeneric(page) — og:image first, then all large <img> tagslooksLikeCarPhoto(url, w, h) — rejects: JUNK_PATTERNS (thumbnails, icons, logos, trackers, SVGs, tiny images), vertical sprites (h/w > 3), min 200x150, square iconsattachImageInterceptor(page) — captures response bytes in-flight for fbcdn.net and craigslist.org images > 5KBimage-size validates buffer: rejects h/w > 2 (portrait sprites), < 200x150 (too small), unreadable bufferstolao/leads/{leadId}/photo-{i}.jpg → permanent res.cloudinary.com URLspatchLead() via API — sets vehicle.photos[] and vehicle.primaryPhotosafePatch() only allows vehicle.photos, vehicle.primaryPhoto, _expired — throws on anything elsenpm run photo-lobster — processes 20 leads per batch, 3–5s polite delay between eachnormalizeApifyLead() reads item.pics || item.images || item.photos || item.imageUrls_50x50c.jpg → _1200x900.jpg, dedup by image ID hash, strip remaining thumbnailsnormalizeFBLead() reads item.images || item.photos || [item.image], slices to 20vehicle.photos[] populated on ingest — no lobster run neededcluster0.kemvuwx.mongodb.nettolaoleads — 248 docs (all source: vettx, pre-archive)tolaoaii_db_user (password in .keys)data/leads.json used when DB not connected (fileReadStore())getAllLeads(): Lead.find({ status: { $ne: "archived" } }) — filters archived leads from dashboard (updated this session)getPendingLeads(): Lead.find({ status: "pending_evaluation" })findLeadByListingUrl(url): Dedup check on ingestsaveLead(record): Upsert by idupdateLead(id, patch): Partial update via $setdeleteLead(id): Hard delete (never for leads with _vAutoAppraisal)clear-bad-photos.mjs: Connects to Atlas, finds leads where vehicle.photos[0] matches cloudinary.*photo-0, sets vehicle.photos: [] and primaryPhoto: nullfix-photo-thumbnails.mjs: Strips 50x50 thumbnails from existing leadsfix-duplicate-photos.mjs: Removes duplicate hero photos across leadsexpire-dead-leads.mjs: Marks leads with dead listing URLs as _expired: true.keys as RAILWAY_TOKENddcmjicij, falls back to local public/photos/https://dashboard.tolao.cogit push origin main (~60s)/api/voice/stream for Twilio ConversationRelayGET /dashboard — Lead cards, filters, detail panel, lightboxGET /lobsters — Lobster Command Center (automation tasks)GET /roadmap — This page (platform architecture)GET /login — Login formGET / — Redirects to /dashboardPOST /api/photos/download — Downloads CDN photos to local diskPOST /api/photos/upload — Multer → Cloudinary upload_streamGET /api/proxy-image — Proxies external image URLsGET /photos/* — Static serve from public/photos/| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | /api/leads | Bearer | All leads (filters archived) |
| GET | /api/leads/:id | Bearer | Single lead detail |
| PATCH | /api/leads/:id | Bearer | Update lead fields |
| DELETE | /api/leads/:id | Bearer | Hard delete lead |
| POST | /api/inbound/listing | Public | Apify CL webhook intake |
| POST | /api/inbound/facebook | Public | Apify FB webhook intake |
| POST | /api/inbound/sms | Public | Twilio inbound SMS |
| POST | /api/inbound/voice | Public | Twilio voice → ConversationRelay |
| POST | /api/inbound/email | Public | SendGrid inbound parse |
| POST | /api/leads/:id/read-plate | Bearer | PlateToVIN plate reader |
| POST | /api/leads/:id/detect-dealer | Bearer | AI dealer detection |
| POST | /api/leads/:id/approve | Bearer | Manager approval → offer_approved |
| POST | /api/leads/:id/capture-contact | Bearer | Save seller contact info |
| POST | /api/vauto/appraise/:id | Bearer | Trigger Playwright vAuto automation |
| POST | /api/vauto/appraisal | Bridge | Bookmarklet captures vAuto values |
| GET | /api/appraisal-queue | Bearer | Leads pending vAuto appraisal |
| POST | /api/admin/run-scraper | Bearer | Trigger Apify actor run |
| POST | /api/admin/backfill-plates | Bearer | Batch PlateToVIN on all leads |
| POST | /api/admin/purge-stale | Bearer | Archive stale/expired leads |
| GET | /api/system/stats | Public | System health + lead counts |
| GET | /api/roadmap/tasks | Public | Roadmap task list |
| POST | /api/outreach/:id/draft | Bearer | AI-generate outreach message |
| POST | /api/outreach/:id/approve | Bearer | Send approved outreach |
processClaim(record, page) — photos → readiness check → appraisal → opening message+15106163508 — SMS_DRY_RUN=true until approvalPOST /api/outreach/:id/draftPOST /api/outreach/:id/approve → sendstatus: "archived" on all source:vettx leads so the dashboard starts clean for FB leads. getAllLeads() already filters archived.curious_coder/facebook-marketplace actor on Apify with fb-actor-input.json config. Set webhook URL to POST /api/inbound/facebook. Leads flow automatically with photos pre-loaded.npm run photo-lobster in batches of 20 until all leads have permanent Cloudinary photos.src/worker.js to run assess → communicate → close pipeline. Requires Chrome with --remote-debugging-port=9222 for vAuto automation.