← back to Sublease Agentabrams
scripts/build-business-plan.js
161 lines
'use strict';
// Generates the Sublease Marketplace business plan PDF, grounded in live CRUnifiedDB counts.
const fs = require('fs');
const path = require('path');
const PDFDocument = require('pdfkit');
const { Pool } = require('pg');
const pool = new Pool({ host: process.env.PGHOST || '/tmp', database: process.env.PGDATABASE || 'crunified' });
const OUT = process.argv[2] || path.join(process.env.HOME, 'Downloads', 'sublease-business-plan.pdf');
const INK = '#14181d', MUT = '#5b6673', ACC = '#1a5f3c', GOLD = '#b08a3e', LINE = '#e0e3e8';
(async () => {
const sponsors = (await pool.query('SELECT * FROM sponsors ORDER BY role, name')).rows;
const [{ n: listings }] = (await pool.query(`SELECT COUNT(*)::int n FROM listings WHERE status='active'`)).rows;
const brokers = sponsors.filter(s => s.role === 'broker');
const lenders = sponsors.filter(s => s.role === 'financing_partner');
const doc = new PDFDocument({ size: 'LETTER', margins: { top: 64, bottom: 64, left: 64, right: 64 } });
const ws = fs.createWriteStream(OUT);
doc.pipe(ws);
const W = doc.page.width - 128;
const H1 = (t) => { doc.moveDown(0.6); doc.fillColor(ACC).font('Helvetica-Bold').fontSize(15).text(t); doc.moveTo(64, doc.y + 2).lineTo(64 + W, doc.y + 2).strokeColor(LINE).stroke(); doc.moveDown(0.5); };
const H2 = (t) => { doc.moveDown(0.3); doc.fillColor(INK).font('Helvetica-Bold').fontSize(11.5).text(t); doc.moveDown(0.15); };
const P = (t) => { doc.fillColor(INK).font('Helvetica').fontSize(10).text(t, { align: 'left', lineGap: 2 }); doc.moveDown(0.35); };
const BUL = (arr) => { arr.forEach(t => doc.fillColor(INK).font('Helvetica').fontSize(10).text('• ' + t, { indent: 6, lineGap: 2 })); doc.moveDown(0.35); };
const NEWPAGE_IF = (need) => { if (doc.y > doc.page.height - 64 - need) doc.addPage(); };
// ── Cover ──
doc.fillColor(INK).font('Helvetica-Bold').fontSize(30).text('Sublease Marketplace', { align: 'left' });
doc.fillColor(GOLD).fontSize(13).text('Business Plan', { align: 'left' });
doc.moveDown(1.2);
doc.fillColor(MUT).font('Helvetica').fontSize(11).text('A gated, broker-sponsored marketplace for commercial subleases & flexible space, backed by CRUnifiedDB — a unified CRE database populated by one automated crawler agent per partner firm.', { lineGap: 3 });
doc.moveDown(1);
doc.fillColor(INK).fontSize(10)
.text('sublease.agentabrams.com', { continued: true }).fillColor(MUT).text(' · access-gated (admin + Boomer)');
doc.fillColor(MUT).fontSize(9).text(`Prepared ${new Date().toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' })} · ${sponsors.length} sponsor firms, ${listings} live listings at draft time`);
doc.moveDown(0.6);
doc.roundedRect(64, doc.y, W, 46, 6).fillAndStroke('#f7f9f8', LINE);
doc.fillColor(MUT).fontSize(8.5).text('Confidential. Listing data is aggregated from public broker pages; cap rates and lease terms are broker-stated and must be independently verified in diligence. CoStar/LoopNet content is gated behind ToS review.', 74, doc.y - 38, { width: W - 20, lineGap: 2 });
doc.moveDown(2);
// ── 1 Executive Summary ──
H1('1 · Executive Summary');
P('The commercial office market is carrying record sublease inventory. Tenants who downsized after 2020 hold long-dated leases they cannot fill; landlords and brokers need a faster channel to move that space. Sublease supply is fragmented across dozens of brokerage sites, national marketplaces, and operator portals — with no single, curated place to see it, and no clean tie to the financing needed to execute a deal.');
P('Sublease Marketplace aggregates sublease and flexible-space listings from the industry\'s leading brokerages and marketplaces into one gated marketplace. Each partner firm is a sponsor with its own dedicated crawler agent that pulls every current listing into a shared database (CRUnifiedDB) on a schedule. Four commercial lenders sit alongside the brokers as financing partners, so a tenant or investor can source the space and the capital in one place.');
BUL([
`${brokers.length} sponsor brokers/marketplaces: ${brokers.map(b => b.name).join(', ')}.`,
`${lenders.length} financing partners: ${lenders.map(l => l.name).join(', ')}.`,
'One automated crawler agent per firm → CRUnifiedDB (Postgres), the single source of truth.',
'Gated access (admin + client "Boomer" login) — a curated, invite-style product, not an open portal.',
]);
// ── 2 The Problem ──
H1('2 · The Problem');
BUL([
'Sublease supply is fragmented: a tenant searching for 5,000 SF in West LA must check LoopNet, CoStar, Crexi, and a dozen individual brokerage sites, each with its own search and none complete.',
'Sublease space is under-marketed: it is a secondary priority for landlords and gets thin listing treatment vs. direct space.',
'No financing tie-in: finding the space and financing the buildout/acquisition are entirely separate journeys.',
'Data goes stale fast: subleases lease up quickly, and static aggregators do not re-crawl often enough to stay current.',
]);
// ── 3 The Solution ──
H1('3 · The Solution');
P('A single gated marketplace with three surfaces: an interactive map, a filterable/sortable listings grid, and a sponsor-broker directory where each firm has its own page and its own live inventory. Behind it, CRUnifiedDB is continuously refreshed by per-firm crawler agents.');
H2('Why "one agent per firm"');
P('Every brokerage publishes listings differently — WordPress + REST for some, Buildout/Catylist embeds for others, JavaScript marketplaces for the largest. A single generic scraper breaks constantly. Instead, each firm gets a dedicated, independently-maintained crawler (the same pattern that runs a large multi-vendor catalog elsewhere in the portfolio): when one firm changes its site, only that one agent needs a fix, and the marketplace stays live.');
H2('Financing partners');
P('The four lenders (Fidelity Mortgage Lenders, Provident Bank, Mizrahi Tefahot / UMTB, Walker Realty Capital) do not list space — they sponsor deals. Each gets a partner page and a "finance this deal" role attached to any listing, turning the marketplace into a two-sided origination channel for them.');
NEWPAGE_IF(200);
// ── 4 Sponsor Roster ──
H1('4 · Sponsor Roster');
doc.font('Helvetica-Bold').fontSize(9).fillColor(MUT);
const cols = [64, 210, 300, 400]; const head = ['Firm', 'Type', 'Role', 'Data / ToS'];
head.forEach((h, i) => doc.text(h, cols[i], doc.y, { continued: i < head.length - 1, width: (cols[i + 1] || 64 + W) - cols[i] }));
doc.moveDown(0.2); doc.moveTo(64, doc.y).lineTo(64 + W, doc.y).strokeColor(LINE).stroke(); doc.moveDown(0.3);
sponsors.forEach(s => {
const y = doc.y; NEWPAGE_IF(20);
doc.font('Helvetica-Bold').fontSize(9).fillColor(INK).text(s.name, cols[0], doc.y, { width: cols[1] - cols[0] - 6 });
const yy = doc.y; doc.font('Helvetica').fontSize(9).fillColor(MUT);
doc.text(s.kind, cols[1], y, { width: cols[2] - cols[1] - 6 });
doc.text(s.role.replace('_', ' '), cols[2], y, { width: cols[3] - cols[2] - 6 });
doc.fillColor(s.tos_risk === 'high' ? '#b3261e' : MUT).text(`ToS: ${s.tos_risk}`, cols[3], y, { width: 64 + W - cols[3] });
doc.y = Math.max(yy, doc.y); doc.moveDown(0.15);
});
doc.moveDown(0.3);
// ── 5 Architecture ──
H1('5 · Technology & Data (CRUnifiedDB)');
BUL([
'CRUnifiedDB — a Postgres database with sponsors, listings, agents, and crawl_runs (an audit + cost trail per run).',
'Per-firm crawler agents write to CRUnifiedDB; a shared toolkit handles polite fetching, robots.txt compliance, free Census/OSM geocoding, and de-duplicated upserts keyed on each firm\'s own listing id.',
'A gated Express app reads CRUnifiedDB and serves the map, grid, and broker pages. No credentials or wholesale data are exposed client-side.',
'Refresh cadence per firm is scheduled; each run records listings found/new and cost (public-page crawling is ~$0).',
]);
H2('Legal & compliance posture');
P('CoStar and LoopNet actively defend their data and run aggressive anti-bot measures. Their agents are ToS-gated: they do not run at volume without an explicit decision, and the plan favors official feeds, partnership/API access, or directory-only treatment for those two rather than adversarial scraping. The lower-risk brokerage and operator sites (NAI Capital, Premier Workspaces, CBRE public search) are crawled from public pages, rate-limited and robots-aware.');
NEWPAGE_IF(240);
// ── 6 Business Model ──
H1('6 · Business Model');
H2('Revenue streams');
BUL([
'Sponsor placement — brokers/marketplaces pay a monthly sponsorship to be featured and to have their inventory aggregated and cross-linked to qualified, gated demand.',
'Financing referral — the four lenders pay per qualified financing lead ("finance this deal") originated off a listing; CRE loan referral fees are high-value.',
'Premium listing — featured placement, top-of-map pins, and enhanced broker pages.',
'Lead-gen / data — anonymized demand signal (what space is being searched, where) sold back to sponsors as market intelligence.',
]);
H2('Why gated (the "Boomer" access model)');
P('Access is invite-style behind a login. That keeps the audience qualified (real principals, brokers, and capital — not tire-kickers), protects sponsor relationships, and lets the product be sold as a curated deal-flow channel rather than a free directory competing head-on with LoopNet.');
// ── 7 Market & Competition ──
H1('7 · Market & Competitive Landscape');
P('National marketplaces (LoopNet, CoStar, Crexi) are broad but not sublease-focused, not curated, and not financing-linked. Individual brokerage sites are authoritative but siloed. Sublease Marketplace is deliberately narrow — subleases + flexible space, curated, gated, and wired to capital partners — which is defensible precisely because the incumbents are structurally horizontal and open.');
BUL([
'Focus: sublease & flexible space only — the segment incumbents under-serve.',
'Curation + gating: a qualified two-sided network, not an open listings dump.',
'Financing tie-in: origination channel the pure-listing incumbents do not offer.',
'Fresh data: per-firm agents re-crawl on cadence, so inventory stays current.',
]);
NEWPAGE_IF(200);
// ── 8 Go-to-market & Roadmap ──
H1('8 · Go-to-Market & Roadmap');
H2('Phase 1 — Foundation (complete / in progress)');
BUL([
'CRUnifiedDB live; 9 sponsors seeded; gated marketplace with map, grid, and broker pages running.',
'First crawler agent live (Premier Workspaces) with real geocoded listings in the database.',
]);
H2('Phase 2 — Coverage');
BUL([
'Stand up the remaining broker agents (NAI Capital via its Buildout search; CBRE public property search).',
'Resolve CoStar/LoopNet posture (partnership/feed vs. directory-only) before any volume crawl.',
'Scheduled refresh per firm + a crawl-health dashboard.',
]);
H2('Phase 3 — Monetization');
BUL([
'Turn on sponsor placement and the "finance this deal" lead flow to the four lenders.',
'Premium listings + market-intelligence reports.',
'Public go-live at sublease.agentabrams.com behind the gated login.',
]);
// ── 9 Risks ──
H1('9 · Key Risks & Mitigations');
BUL([
'Scraping / ToS (CoStar, LoopNet): mitigate via ToS-gating, official feeds/partnerships, robots compliance, and directory-only fallback.',
'Data freshness: per-firm scheduled re-crawls + last-seen tracking; stale listings auto-expire.',
'Sponsor concentration: diversify across brokers, operators, and lenders so no single firm is load-bearing.',
'Liability on listing accuracy: clear "broker-stated, verify in diligence" disclaimers throughout.',
]);
doc.moveDown(1);
doc.fillColor(MUT).font('Helvetica-Oblique').fontSize(8.5).text('Generated from CRUnifiedDB. Counts reflect the database at generation time and grow as broker agents run.', { align: 'center' });
doc.end();
await new Promise((resolve, reject) => { ws.on('finish', resolve); ws.on('error', reject); });
await pool.end();
console.log('PDF written →', OUT);
})();