Vibecode MarketMaster
track this build4 phases, 12 steps, beginner friendly0%A solo campaign generator that scrapes one site and prompts an LLM for coordinated copy is a realistic weekend build. MarketMaster's paid value is the deeper research, multi-format media generation, deterministic quality gates, independent review, integrations, scheduling, and maintained publishing workflow.
You are building a lean indie version of MarketMaster. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # MarketMaster · indie build A campaign generator whose hard problems are extracting a brief from one website without becoming a crawler, keeping every generated line traceable to an approved fact, and enforcing platform limits in code because the model cannot count. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Node.js 22 with Express and server-rendered HTML on localhost | no accounts, no build step, nothing to operate | | Storage | better-sqlite3, briefs and campaigns with their approval state | the approval state is the product, so it belongs in a transaction | | Fetching | Same-domain HTML only, a page cap, a declared user agent, cached to disk | one polite pass over one site, not a crawler | | Model | One provider chosen in the environment, structured output validated on arrival | prose in, structured fields out, or discard and retry | | Checks | Deterministic validators in code, run on every asset every time | counting characters is not a job for a language model | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the server and the generation worker. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **An API key for one text model** · cents per campaign at current small-model prices, but set the cap anyway - Why: Generation needs exactly one provider, and it should carry a hard spending cap because a retry loop is the usual way a bill happens. - Get it: Create a key with your chosen provider and set a monthly limit on the account. - Verify: A campaign generates and the estimated cost is recorded - [ ] **The current character limits for each channel you target** · free - Why: These are the checks the whole build exists to run, and a limit you remembered from last year is the reason an upload gets rejected. - Get it: Read each platform's own specification page and copy the numbers into configuration with the date and the URL. - Verify: Configuration holds every limit with its source URL and check date - [ ] **A website you are allowed to extract from** · free - Why: Your own site or a client's, with permission, because the tool fetches real pages and stores their text on your machine. - Get it: Use your own domain, or get written permission for a client's. - Verify: A brief extracts from it inside the page cap - [ ] **Your daily spend ceiling and retry cap** · free - Why: Both are enforced in code, and choosing them after an expensive night is choosing them too late. - Get it: Write down the maximum estimated spend per day and the maximum retries per asset. - Verify: Both are in configuration and named in tests ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Image and video generation. - Publishing and scheduling integrations. - Multi-model review. - Any predicted performance figure. - multi-model campaign review - generated image and video assets - live social and email integrations - scheduled publishing and delivery history - maintained ad-pattern and platform-limit references If one of those is essential to you, that is the reason to keep paying for MarketMaster, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · MarketMaster The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a campaign copy generator that will not emit a claim the approved brief does not support to replace MarketMaster. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero One rule, and it is the reason this is worth building rather than pasting a prompt into a chat window. Every sentence the generator produces must be traceable to a field a human approved. A language model asked to write an advertisement will produce social proof, growth figures and awards because that is what advertisements contain, and the invented ones read exactly like the real ones. So the flow is: extract, show the brief, let a person edit and approve it, then generate only from approved fields, and record on every asset which fields it drew on. Any number, superlative or named customer that is not in the brief is flagged beside the asset and the asset cannot be marked approved until it is resolved. The second half of that rule: checks that matter are written in code, not asked of the model. Character limits, missing calls to action and unsupported figures are arithmetic and string matching. A model asked whether its own headline fits in thirty characters will tell you yes. ### Stack (fixed, do not substitute) - Runtime: Node.js 22 with Express and server-rendered HTML on localhost · no accounts, no build step, nothing to operate - Storage: better-sqlite3, briefs and campaigns with their approval state · the approval state is the product, so it belongs in a transaction - Fetching: Same-domain HTML only, a page cap, a declared user agent, cached to disk · one polite pass over one site, not a crawler - Model: One provider chosen in the environment, structured output validated on arrival · prose in, structured fields out, or discard and retry - Checks: Deterministic validators in code, run on every asset every time · counting characters is not a job for a language model ### Data model (create this before Phase 1) - sources(id, site_root, url, fetched_at, status, bytes, cache_path) · one row per page actually fetched - briefs(id, site_root, fields_json, approved_at, approved_by_note) · nothing generates until approved_at is set - facts(brief_id, key, value, evidence_url) · the only numbers and named customers allowed to appear - campaigns(id, brief_id, model_name, prompt_version, created_at, token_cost_estimate) - assets(id, campaign_id, channel, kind, text, fields_used_json, state) · state is draft, flagged or approved - findings(id, asset_id, rule, detail, severity) · one row per failed check, cleared only by an edit - An asset with an open finding of blocking severity cannot reach approved, enforced in the database write rather than in the template. ### Phase 1 · One polite pass over one website Build: Accept one site root. Fetch its HTML pages on the same domain only, up to a page cap, with a declared user agent naming the tool and a delay between requests. Honour the site's crawl directives and any noindex it declares. Cache every response to disk so a rerun fetches nothing, and extract visible text only, dropping navigation, scripts and styles. Trap: Following every link on the page turns a brief extractor into an unannounced crawler that hits a small site a thousand times in a minute, and the first you hear of it is a blocked address. Cap the page count, sleep between requests, identify yourself, and cache so the second run is free. Done when: A run fetches no more than the configured page cap and no URL outside the given domain, tested with a fixture site containing external and subdomain links. The user agent string identifies the tool and appears in the README. A second run over the same site issues no network requests, verified by counting them. Extracted text excludes script, style and navigation content, checked against a fixture. Do not build yet: No brief, no generation, no checks or exports. ### Phase 2 · A brief a person approved, field by field Build: Extract proposed values for product, audience, pricing, proof points, brand voice and call to action, each with the source URL and the exact sentence it came from. Present them in an editable form. Approval writes approved_at and freezes the fields. Every number or named customer the person keeps becomes a row in the facts table with its evidence link. Trap: Letting generation run straight from extraction means a marketing page's own puffery becomes your source of truth, and a phrase the site's designer wrote as filler is now the proof point in three advertisements. Make approval a required step with the source sentence beside each field, so keeping a claim is a decision somebody made. Done when: Generation is refused for a brief with no approved_at, tested through the route. Every proposed field displays its source URL and source sentence. Approved numbers and named customers appear in the facts table with an evidence link, and a fact without evidence cannot be saved. Editing an approved brief clears the approval and requires a new one. Do not build yet: No asset generation, checks or exports yet. ### Phase 3 · Assets that say where each line came from Build: Generate a fixed set per campaign: three advertisement concepts, five social posts, a five-slide carousel outline, a short video script and one email. Require the model to return structured assets naming the brief fields each one drew on, validate against the schema, and discard and retry a bounded number of times rather than storing anything partial. Trap: Accepting free prose and parsing headlines out of it with a regular expression works until the model returns four headlines under a heading you did not ask for, and now half your assets are fragments of a bullet list. Demand structured output, validate it, and treat a malformed response as a retry rather than as data. Done when: Every stored asset lists the brief fields it used and each named field exists in the approved brief. A malformed model response is discarded with the reason logged and never partially stored, tested with a crafted response. Regenerating one asset leaves the others untouched. The model name and prompt version are recorded on the campaign so a result can be explained later. Do not build yet: No validation checks, approval or export yet. ### Phase 4 · Checks written in code, because the model cannot count Build: Write validators in code. For search advertisements, Google's own specification currently allows one to fifteen headlines of up to thirty characters, one to four descriptions of up to ninety characters and a final URL of up to two thousand and forty eight characters: read the current page, put the numbers in configuration with the date you checked, and fail an asset that exceeds them. Flag any number or named entity absent from the facts table, and any asset with no call to action. Trap: Asking the model to keep headlines under thirty characters and trusting the answer produces a rejected upload every time, because a model predicting text is not counting it, and the limits differ per channel besides. Count in code against limits you read from the platform's documentation and stored with the date you read them. Done when: An asset one character over a configured limit is flagged with the measured length and the limit shown. A number absent from the facts table is flagged with the phrase highlighted, tested with a crafted asset. An asset with a blocking finding cannot be marked approved, enforced at the database write and tested through the route. The configuration records each limit with the date and the source URL it came from. Do not build yet: No export or spend controls yet. ### Phase 5 · Approval, export and a ceiling on what a run can cost Build: Export the approved package as Markdown and JSON including findings, fields used, model name and the brief it came from. Estimate token cost before a run and refuse to start above a configured ceiling. Keep a running total per day. Store fetched page text and keys locally and bind the server to localhost. Trap: A retry loop around a validation failure will happily regenerate a campaign forty times overnight against a metered API, and the first sign is the bill. Cap retries, estimate before you call, refuse above a daily ceiling, and show the running total on the page where the button is. Done when: Export produces Markdown and JSON containing every approved asset with its findings, fields used and model name, and the JSON reimports into the app to reconstruct the campaign. A run whose estimate exceeds the ceiling is refused before any API call, tested. The daily spend estimate is visible beside the generate button. The server refuses connections from anything but localhost, verified. Do not build yet: Nothing after this. ### Out of scope (and why) - Image and video generation, which multiplies the cost per run by a large factor and adds a rights question to every asset. - Publishing and scheduling to advertising and social platforms, each of which is an approval process and an integration project of its own. - Multi-model review, where a second model grades the first, because agreement between two models is not evidence about the world. - Any claim about expected performance, click rates or conversion, since nothing in this tool has ever seen a result. ### README must contain - That nothing generates until a person has approved the brief, and that editing it clears approval - That every number and named customer must exist in the facts table with an evidence link - That character limits are counted in code, where the limits came from, and the date they were checked - The fetching etiquette in use: same domain, page cap, declared user agent, delay and cache - The cost ceiling, how the estimate is made, and that the server binds to localhost ===== AGENTS.md ===== # Agent instructions · MarketMaster indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Node.js 22 with Express and server-rendered HTML on localhost, better-sqlite3, briefs and campaigns with their approval state, Same-domain HTML only, a page cap, a declared user agent, cached to disk, One provider chosen in the environment, structured output validated on arrival, Deterministic validators in code, run on every asset every time. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Following every link on the page turns a brief extractor into an unannounced crawler that hits a small site a thousand times in a minute, and the first you hear of it is a blocked address. Cap the page count, sleep between requests, identify yourself, and cache so the second run is free. - Letting generation run straight from extraction means a marketing page's own puffery becomes your source of truth, and a phrase the site's designer wrote as filler is now the proof point in three advertisements. Make approval a required step with the source sentence beside each field, so keeping a claim is a decision somebody made. - Accepting free prose and parsing headlines out of it with a regular expression works until the model returns four headlines under a heading you did not ask for, and now half your assets are fragments of a bullet list. Demand structured output, validate it, and treat a malformed response as a retry rather than as data. - Asking the model to keep headlines under thirty characters and trusting the answer produces a rejected upload every time, because a model predicting text is not counting it, and the limits differ per channel besides. Count in code against limits you read from the platform's documentation and stored with the date you read them. ===== BUILD_PLAN.md ===== # Build plan · MarketMaster A campaign generator whose hard problems are extracting a brief from one website without becoming a crawler, keeping every generated line traceable to an approved fact, and enforcing platform limits in code because the model cannot count. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · One polite pass over one website Fetching is bounded, identifiable, cached and stops at the domain you were given. ### Steps 1. Cap pages and stay on the domain Two lines of code and the difference between a tool and a nuisance. Files: `src/fetch/crawl.js`, `test/crawl.test.js` 2. Identify yourself and pause between requests Somebody is paying for the bandwidth you are using. Files: `src/fetch/client.js` 3. Cache to disk and prove the rerun is free You will rerun this twenty times while tuning the prompt. Files: `src/fetch/cache.js` ### Done when - [ ] No fetch leaves the given domain and the cap holds - [ ] The user agent names the tool - [ ] A rerun issues no network requests - [ ] Extracted text excludes scripts, styles and navigation ### Watch out - Following every link on the page turns a brief extractor into an unannounced crawler that hits a small site a thousand times in a minute, and the first you hear of it is a blocked address. Cap the page count, sleep between requests, identify yourself, and cache so the second run is free. ## Phase 2 · A brief a person approved, field by field Extraction proposes, a human disposes, and nothing generates before approval. ### Steps 1. Carry the source sentence with every field Approval without the evidence in front of you is a rubber stamp. Files: `src/extract/fields.js` 2. Refuse generation before approval Checked in the route, not in the interface. Files: `src/routes/campaign.js` 3. Turn approved claims into facts rows The next phase checks against this table and nothing else. Files: `src/db/facts.js` ### Done when - [ ] Generation is refused without approval - [ ] Every field shows its source URL and sentence - [ ] Facts require an evidence link - [ ] Editing a brief clears its approval ### Watch out - Letting generation run straight from extraction means a marketing page's own puffery becomes your source of truth, and a phrase the site's designer wrote as filler is now the proof point in three advertisements. Make approval a required step with the source sentence beside each field, so keeping a claim is a decision somebody made. ## Phase 3 · Assets that say where each line came from Every generated asset records the brief fields it used. ### Steps 1. Demand structured assets with field references The reference is what makes the next phase's claim check possible. Files: `src/generate/assets.js` 2. Validate and retry, never salvage A half-parsed advertisement is worse than no advertisement. Files: `src/generate/validate.js`, `test/generate.test.js` 3. Record model and prompt version In three weeks you will not remember which prompt produced this. Files: `src/db/campaigns.js` ### Done when - [ ] Each asset names brief fields that actually exist - [ ] Malformed responses are discarded and retried - [ ] Regenerating one asset leaves others untouched - [ ] Model and prompt version are recorded ### Watch out - Accepting free prose and parsing headlines out of it with a regular expression works until the model returns four headlines under a heading you did not ask for, and now half your assets are fragments of a bullet list. Demand structured output, validate it, and treat a malformed response as a retry rather than as data. ## Phase 4 · Checks written in code, because the model cannot count Length, claims and calls to action are validated deterministically against published limits. ### Steps 1. Count characters in code, per channel The model's opinion about its own length is not evidence. Files: `src/checks/limits.js`, `test/limits.test.js` 2. Flag every number not in the facts table Highlighted in the asset, not summarised in a log. Files: `src/checks/claims.js` 3. Block approval at the write A rule enforced only in the template is a suggestion. Files: `src/db/assets.js` ### Done when - [ ] Over-length assets are flagged with measured length - [ ] Unsupported numbers are flagged in place - [ ] Blocking findings prevent approval at the database layer - [ ] Limits carry their source URL and check date ### Watch out - Asking the model to keep headlines under thirty characters and trusting the answer produces a rejected upload every time, because a model predicting text is not counting it, and the limits differ per channel besides. Count in code against limits you read from the platform's documentation and stored with the date you read them. ## Not in this build - Image and video generation. - Publishing and scheduling integrations. - Multi-model review. - Any predicted performance figure. ## After v1, if you want it - A second channel's limits added as configuration rather than code, once the first set has been checked twice - A diff view between two campaigns from the same brief, which makes prompt changes visible - A brief that merges several approved sources, once single-source extraction has been used in anger ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Localhost only. The app is a tool, not a service. PORT=4860 # Required. Briefs, facts, campaigns, assets and findings. DATABASE_PATH=./data/campaigns.db # Required. Chosen once. Recorded on every campaign. MODEL_PROVIDER=one provider name # Required · secret. Never logged, never written into an export. MODEL_API_KEY=sk-...redacted # Required. Maximum pages fetched per site, per run. FETCH_PAGE_CAP=25 # Required. Identifies you to the site you are fetching from. FETCH_USER_AGENT=campaign-brief-tool/1.0 (contact: you@example.com) # Required. Estimated. A run above it is refused before any API call. DAILY_SPEND_CEILING=2.00 # Required. The other half of not spending a fortune overnight. MAX_RETRIES_PER_ASSET=3
You are building a lean indie version of MarketMaster. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # MarketMaster · indie build A campaign generator whose hard problems are extracting a brief from one website without becoming a crawler, keeping every generated line traceable to an approved fact, and enforcing platform limits in code because the model cannot count. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Node.js 22 with Express and server-rendered HTML on localhost | no accounts, no build step, nothing to operate | | Storage | better-sqlite3, briefs and campaigns with their approval state | the approval state is the product, so it belongs in a transaction | | Fetching | Same-domain HTML only, a page cap, a declared user agent, cached to disk | one polite pass over one site, not a crawler | | Model | One provider chosen in the environment, structured output validated on arrival | prose in, structured fields out, or discard and retry | | Checks | Deterministic validators in code, run on every asset every time | counting characters is not a job for a language model | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the server and the generation worker. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **An API key for one text model** · cents per campaign at current small-model prices, but set the cap anyway - Why: Generation needs exactly one provider, and it should carry a hard spending cap because a retry loop is the usual way a bill happens. - Get it: Create a key with your chosen provider and set a monthly limit on the account. - Verify: A campaign generates and the estimated cost is recorded - [ ] **The current character limits for each channel you target** · free - Why: These are the checks the whole build exists to run, and a limit you remembered from last year is the reason an upload gets rejected. - Get it: Read each platform's own specification page and copy the numbers into configuration with the date and the URL. - Verify: Configuration holds every limit with its source URL and check date - [ ] **A website you are allowed to extract from** · free - Why: Your own site or a client's, with permission, because the tool fetches real pages and stores their text on your machine. - Get it: Use your own domain, or get written permission for a client's. - Verify: A brief extracts from it inside the page cap - [ ] **Your daily spend ceiling and retry cap** · free - Why: Both are enforced in code, and choosing them after an expensive night is choosing them too late. - Get it: Write down the maximum estimated spend per day and the maximum retries per asset. - Verify: Both are in configuration and named in tests ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Image and video generation. - Publishing and scheduling integrations. - Multi-model review. - Any predicted performance figure. - multi-model campaign review - generated image and video assets - live social and email integrations - scheduled publishing and delivery history - maintained ad-pattern and platform-limit references If one of those is essential to you, that is the reason to keep paying for MarketMaster, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · MarketMaster The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a campaign copy generator that will not emit a claim the approved brief does not support to replace MarketMaster. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero One rule, and it is the reason this is worth building rather than pasting a prompt into a chat window. Every sentence the generator produces must be traceable to a field a human approved. A language model asked to write an advertisement will produce social proof, growth figures and awards because that is what advertisements contain, and the invented ones read exactly like the real ones. So the flow is: extract, show the brief, let a person edit and approve it, then generate only from approved fields, and record on every asset which fields it drew on. Any number, superlative or named customer that is not in the brief is flagged beside the asset and the asset cannot be marked approved until it is resolved. The second half of that rule: checks that matter are written in code, not asked of the model. Character limits, missing calls to action and unsupported figures are arithmetic and string matching. A model asked whether its own headline fits in thirty characters will tell you yes. ### Stack (fixed, do not substitute) - Runtime: Node.js 22 with Express and server-rendered HTML on localhost · no accounts, no build step, nothing to operate - Storage: better-sqlite3, briefs and campaigns with their approval state · the approval state is the product, so it belongs in a transaction - Fetching: Same-domain HTML only, a page cap, a declared user agent, cached to disk · one polite pass over one site, not a crawler - Model: One provider chosen in the environment, structured output validated on arrival · prose in, structured fields out, or discard and retry - Checks: Deterministic validators in code, run on every asset every time · counting characters is not a job for a language model ### Data model (create this before Phase 1) - sources(id, site_root, url, fetched_at, status, bytes, cache_path) · one row per page actually fetched - briefs(id, site_root, fields_json, approved_at, approved_by_note) · nothing generates until approved_at is set - facts(brief_id, key, value, evidence_url) · the only numbers and named customers allowed to appear - campaigns(id, brief_id, model_name, prompt_version, created_at, token_cost_estimate) - assets(id, campaign_id, channel, kind, text, fields_used_json, state) · state is draft, flagged or approved - findings(id, asset_id, rule, detail, severity) · one row per failed check, cleared only by an edit - An asset with an open finding of blocking severity cannot reach approved, enforced in the database write rather than in the template. ### Phase 1 · One polite pass over one website Build: Accept one site root. Fetch its HTML pages on the same domain only, up to a page cap, with a declared user agent naming the tool and a delay between requests. Honour the site's crawl directives and any noindex it declares. Cache every response to disk so a rerun fetches nothing, and extract visible text only, dropping navigation, scripts and styles. Trap: Following every link on the page turns a brief extractor into an unannounced crawler that hits a small site a thousand times in a minute, and the first you hear of it is a blocked address. Cap the page count, sleep between requests, identify yourself, and cache so the second run is free. Done when: A run fetches no more than the configured page cap and no URL outside the given domain, tested with a fixture site containing external and subdomain links. The user agent string identifies the tool and appears in the README. A second run over the same site issues no network requests, verified by counting them. Extracted text excludes script, style and navigation content, checked against a fixture. Do not build yet: No brief, no generation, no checks or exports. ### Phase 2 · A brief a person approved, field by field Build: Extract proposed values for product, audience, pricing, proof points, brand voice and call to action, each with the source URL and the exact sentence it came from. Present them in an editable form. Approval writes approved_at and freezes the fields. Every number or named customer the person keeps becomes a row in the facts table with its evidence link. Trap: Letting generation run straight from extraction means a marketing page's own puffery becomes your source of truth, and a phrase the site's designer wrote as filler is now the proof point in three advertisements. Make approval a required step with the source sentence beside each field, so keeping a claim is a decision somebody made. Done when: Generation is refused for a brief with no approved_at, tested through the route. Every proposed field displays its source URL and source sentence. Approved numbers and named customers appear in the facts table with an evidence link, and a fact without evidence cannot be saved. Editing an approved brief clears the approval and requires a new one. Do not build yet: No asset generation, checks or exports yet. ### Phase 3 · Assets that say where each line came from Build: Generate a fixed set per campaign: three advertisement concepts, five social posts, a five-slide carousel outline, a short video script and one email. Require the model to return structured assets naming the brief fields each one drew on, validate against the schema, and discard and retry a bounded number of times rather than storing anything partial. Trap: Accepting free prose and parsing headlines out of it with a regular expression works until the model returns four headlines under a heading you did not ask for, and now half your assets are fragments of a bullet list. Demand structured output, validate it, and treat a malformed response as a retry rather than as data. Done when: Every stored asset lists the brief fields it used and each named field exists in the approved brief. A malformed model response is discarded with the reason logged and never partially stored, tested with a crafted response. Regenerating one asset leaves the others untouched. The model name and prompt version are recorded on the campaign so a result can be explained later. Do not build yet: No validation checks, approval or export yet. ### Phase 4 · Checks written in code, because the model cannot count Build: Write validators in code. For search advertisements, Google's own specification currently allows one to fifteen headlines of up to thirty characters, one to four descriptions of up to ninety characters and a final URL of up to two thousand and forty eight characters: read the current page, put the numbers in configuration with the date you checked, and fail an asset that exceeds them. Flag any number or named entity absent from the facts table, and any asset with no call to action. Trap: Asking the model to keep headlines under thirty characters and trusting the answer produces a rejected upload every time, because a model predicting text is not counting it, and the limits differ per channel besides. Count in code against limits you read from the platform's documentation and stored with the date you read them. Done when: An asset one character over a configured limit is flagged with the measured length and the limit shown. A number absent from the facts table is flagged with the phrase highlighted, tested with a crafted asset. An asset with a blocking finding cannot be marked approved, enforced at the database write and tested through the route. The configuration records each limit with the date and the source URL it came from. Do not build yet: No export or spend controls yet. ### Phase 5 · Approval, export and a ceiling on what a run can cost Build: Export the approved package as Markdown and JSON including findings, fields used, model name and the brief it came from. Estimate token cost before a run and refuse to start above a configured ceiling. Keep a running total per day. Store fetched page text and keys locally and bind the server to localhost. Trap: A retry loop around a validation failure will happily regenerate a campaign forty times overnight against a metered API, and the first sign is the bill. Cap retries, estimate before you call, refuse above a daily ceiling, and show the running total on the page where the button is. Done when: Export produces Markdown and JSON containing every approved asset with its findings, fields used and model name, and the JSON reimports into the app to reconstruct the campaign. A run whose estimate exceeds the ceiling is refused before any API call, tested. The daily spend estimate is visible beside the generate button. The server refuses connections from anything but localhost, verified. Do not build yet: Nothing after this. ### Out of scope (and why) - Image and video generation, which multiplies the cost per run by a large factor and adds a rights question to every asset. - Publishing and scheduling to advertising and social platforms, each of which is an approval process and an integration project of its own. - Multi-model review, where a second model grades the first, because agreement between two models is not evidence about the world. - Any claim about expected performance, click rates or conversion, since nothing in this tool has ever seen a result. ### README must contain - That nothing generates until a person has approved the brief, and that editing it clears approval - That every number and named customer must exist in the facts table with an evidence link - That character limits are counted in code, where the limits came from, and the date they were checked - The fetching etiquette in use: same domain, page cap, declared user agent, delay and cache - The cost ceiling, how the estimate is made, and that the server binds to localhost ===== AGENTS.md ===== # Agent instructions · MarketMaster indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Node.js 22 with Express and server-rendered HTML on localhost, better-sqlite3, briefs and campaigns with their approval state, Same-domain HTML only, a page cap, a declared user agent, cached to disk, One provider chosen in the environment, structured output validated on arrival, Deterministic validators in code, run on every asset every time. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Following every link on the page turns a brief extractor into an unannounced crawler that hits a small site a thousand times in a minute, and the first you hear of it is a blocked address. Cap the page count, sleep between requests, identify yourself, and cache so the second run is free. - Letting generation run straight from extraction means a marketing page's own puffery becomes your source of truth, and a phrase the site's designer wrote as filler is now the proof point in three advertisements. Make approval a required step with the source sentence beside each field, so keeping a claim is a decision somebody made. - Accepting free prose and parsing headlines out of it with a regular expression works until the model returns four headlines under a heading you did not ask for, and now half your assets are fragments of a bullet list. Demand structured output, validate it, and treat a malformed response as a retry rather than as data. - Asking the model to keep headlines under thirty characters and trusting the answer produces a rejected upload every time, because a model predicting text is not counting it, and the limits differ per channel besides. Count in code against limits you read from the platform's documentation and stored with the date you read them. ===== BUILD_PLAN.md ===== # Build plan · MarketMaster A campaign generator whose hard problems are extracting a brief from one website without becoming a crawler, keeping every generated line traceable to an approved fact, and enforcing platform limits in code because the model cannot count. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · One polite pass over one website Fetching is bounded, identifiable, cached and stops at the domain you were given. ### Steps 1. Cap pages and stay on the domain Two lines of code and the difference between a tool and a nuisance. Files: `src/fetch/crawl.js`, `test/crawl.test.js` 2. Identify yourself and pause between requests Somebody is paying for the bandwidth you are using. Files: `src/fetch/client.js` 3. Cache to disk and prove the rerun is free You will rerun this twenty times while tuning the prompt. Files: `src/fetch/cache.js` ### Done when - [ ] No fetch leaves the given domain and the cap holds - [ ] The user agent names the tool - [ ] A rerun issues no network requests - [ ] Extracted text excludes scripts, styles and navigation ### Watch out - Following every link on the page turns a brief extractor into an unannounced crawler that hits a small site a thousand times in a minute, and the first you hear of it is a blocked address. Cap the page count, sleep between requests, identify yourself, and cache so the second run is free. ## Phase 2 · A brief a person approved, field by field Extraction proposes, a human disposes, and nothing generates before approval. ### Steps 1. Carry the source sentence with every field Approval without the evidence in front of you is a rubber stamp. Files: `src/extract/fields.js` 2. Refuse generation before approval Checked in the route, not in the interface. Files: `src/routes/campaign.js` 3. Turn approved claims into facts rows The next phase checks against this table and nothing else. Files: `src/db/facts.js` ### Done when - [ ] Generation is refused without approval - [ ] Every field shows its source URL and sentence - [ ] Facts require an evidence link - [ ] Editing a brief clears its approval ### Watch out - Letting generation run straight from extraction means a marketing page's own puffery becomes your source of truth, and a phrase the site's designer wrote as filler is now the proof point in three advertisements. Make approval a required step with the source sentence beside each field, so keeping a claim is a decision somebody made. ## Phase 3 · Assets that say where each line came from Every generated asset records the brief fields it used. ### Steps 1. Demand structured assets with field references The reference is what makes the next phase's claim check possible. Files: `src/generate/assets.js` 2. Validate and retry, never salvage A half-parsed advertisement is worse than no advertisement. Files: `src/generate/validate.js`, `test/generate.test.js` 3. Record model and prompt version In three weeks you will not remember which prompt produced this. Files: `src/db/campaigns.js` ### Done when - [ ] Each asset names brief fields that actually exist - [ ] Malformed responses are discarded and retried - [ ] Regenerating one asset leaves others untouched - [ ] Model and prompt version are recorded ### Watch out - Accepting free prose and parsing headlines out of it with a regular expression works until the model returns four headlines under a heading you did not ask for, and now half your assets are fragments of a bullet list. Demand structured output, validate it, and treat a malformed response as a retry rather than as data. ## Phase 4 · Checks written in code, because the model cannot count Length, claims and calls to action are validated deterministically against published limits. ### Steps 1. Count characters in code, per channel The model's opinion about its own length is not evidence. Files: `src/checks/limits.js`, `test/limits.test.js` 2. Flag every number not in the facts table Highlighted in the asset, not summarised in a log. Files: `src/checks/claims.js` 3. Block approval at the write A rule enforced only in the template is a suggestion. Files: `src/db/assets.js` ### Done when - [ ] Over-length assets are flagged with measured length - [ ] Unsupported numbers are flagged in place - [ ] Blocking findings prevent approval at the database layer - [ ] Limits carry their source URL and check date ### Watch out - Asking the model to keep headlines under thirty characters and trusting the answer produces a rejected upload every time, because a model predicting text is not counting it, and the limits differ per channel besides. Count in code against limits you read from the platform's documentation and stored with the date you read them. ## Not in this build - Image and video generation. - Publishing and scheduling integrations. - Multi-model review. - Any predicted performance figure. ## After v1, if you want it - A second channel's limits added as configuration rather than code, once the first set has been checked twice - A diff view between two campaigns from the same brief, which makes prompt changes visible - A brief that merges several approved sources, once single-source extraction has been used in anger ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Localhost only. The app is a tool, not a service. PORT=4860 # Required. Briefs, facts, campaigns, assets and findings. DATABASE_PATH=./data/campaigns.db # Required. Chosen once. Recorded on every campaign. MODEL_PROVIDER=one provider name # Required · secret. Never logged, never written into an export. MODEL_API_KEY=sk-...redacted # Required. Maximum pages fetched per site, per run. FETCH_PAGE_CAP=25 # Required. Identifies you to the site you are fetching from. FETCH_USER_AGENT=campaign-brief-tool/1.0 (contact: you@example.com) # Required. Estimated. A run above it is refused before any API call. DAILY_SPEND_CEILING=2.00 # Required. The other half of not spending a fortune overnight. MAX_RETRIES_PER_ASSET=3
You are building a production product version of MarketMaster. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # MarketMaster · product brief ## Problem A solo campaign generator that scrapes one site and prompts an LLM for coordinated copy is a realistic weekend build. MarketMaster's paid value is the deeper research, multi-format media generation, deterministic quality gates, independent review, integrations, scheduling, and maintained publishing workflow. ## Product outcome A generator that reads one site politely, will not write a sentence the approved brief does not support, counts characters in code rather than asking the model, and cannot spend more than you allowed it to. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - LLM API key - website text extraction - SQLite - hosted or local web app ## Explicit non-goals for v1 - Image and video generation. - Publishing and scheduling integrations. - Multi-model review. - Any predicted performance figure. - multi-model campaign review - generated image and video assets - live social and email integrations - scheduled publishing and delivery history - maintained ad-pattern and platform-limit references ## Success criteria - Fetching is confirmed to stay on the domain and inside the cap - Generation is confirmed refused before brief approval - An over-length asset is confirmed flagged with its measured length - A blocking finding is confirmed to prevent approval at the database layer - A run above the spend ceiling is confirmed refused before any API call ===== BRIEF.md ===== # Build brief · MarketMaster The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a campaign copy generator that will not emit a claim the approved brief does not support to replace MarketMaster. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero One rule, and it is the reason this is worth building rather than pasting a prompt into a chat window. Every sentence the generator produces must be traceable to a field a human approved. A language model asked to write an advertisement will produce social proof, growth figures and awards because that is what advertisements contain, and the invented ones read exactly like the real ones. So the flow is: extract, show the brief, let a person edit and approve it, then generate only from approved fields, and record on every asset which fields it drew on. Any number, superlative or named customer that is not in the brief is flagged beside the asset and the asset cannot be marked approved until it is resolved. The second half of that rule: checks that matter are written in code, not asked of the model. Character limits, missing calls to action and unsupported figures are arithmetic and string matching. A model asked whether its own headline fits in thirty characters will tell you yes. ### Stack (fixed, do not substitute) - Runtime: Node.js 22 with Express and server-rendered HTML on localhost · no accounts, no build step, nothing to operate - Storage: better-sqlite3, briefs and campaigns with their approval state · the approval state is the product, so it belongs in a transaction - Fetching: Same-domain HTML only, a page cap, a declared user agent, cached to disk · one polite pass over one site, not a crawler - Model: One provider chosen in the environment, structured output validated on arrival · prose in, structured fields out, or discard and retry - Checks: Deterministic validators in code, run on every asset every time · counting characters is not a job for a language model ### Data model (create this before Phase 1) - sources(id, site_root, url, fetched_at, status, bytes, cache_path) · one row per page actually fetched - briefs(id, site_root, fields_json, approved_at, approved_by_note) · nothing generates until approved_at is set - facts(brief_id, key, value, evidence_url) · the only numbers and named customers allowed to appear - campaigns(id, brief_id, model_name, prompt_version, created_at, token_cost_estimate) - assets(id, campaign_id, channel, kind, text, fields_used_json, state) · state is draft, flagged or approved - findings(id, asset_id, rule, detail, severity) · one row per failed check, cleared only by an edit - An asset with an open finding of blocking severity cannot reach approved, enforced in the database write rather than in the template. ### Phase 1 · One polite pass over one website Build: Accept one site root. Fetch its HTML pages on the same domain only, up to a page cap, with a declared user agent naming the tool and a delay between requests. Honour the site's crawl directives and any noindex it declares. Cache every response to disk so a rerun fetches nothing, and extract visible text only, dropping navigation, scripts and styles. Trap: Following every link on the page turns a brief extractor into an unannounced crawler that hits a small site a thousand times in a minute, and the first you hear of it is a blocked address. Cap the page count, sleep between requests, identify yourself, and cache so the second run is free. Done when: A run fetches no more than the configured page cap and no URL outside the given domain, tested with a fixture site containing external and subdomain links. The user agent string identifies the tool and appears in the README. A second run over the same site issues no network requests, verified by counting them. Extracted text excludes script, style and navigation content, checked against a fixture. Do not build yet: No brief, no generation, no checks or exports. ### Phase 2 · A brief a person approved, field by field Build: Extract proposed values for product, audience, pricing, proof points, brand voice and call to action, each with the source URL and the exact sentence it came from. Present them in an editable form. Approval writes approved_at and freezes the fields. Every number or named customer the person keeps becomes a row in the facts table with its evidence link. Trap: Letting generation run straight from extraction means a marketing page's own puffery becomes your source of truth, and a phrase the site's designer wrote as filler is now the proof point in three advertisements. Make approval a required step with the source sentence beside each field, so keeping a claim is a decision somebody made. Done when: Generation is refused for a brief with no approved_at, tested through the route. Every proposed field displays its source URL and source sentence. Approved numbers and named customers appear in the facts table with an evidence link, and a fact without evidence cannot be saved. Editing an approved brief clears the approval and requires a new one. Do not build yet: No asset generation, checks or exports yet. ### Phase 3 · Assets that say where each line came from Build: Generate a fixed set per campaign: three advertisement concepts, five social posts, a five-slide carousel outline, a short video script and one email. Require the model to return structured assets naming the brief fields each one drew on, validate against the schema, and discard and retry a bounded number of times rather than storing anything partial. Trap: Accepting free prose and parsing headlines out of it with a regular expression works until the model returns four headlines under a heading you did not ask for, and now half your assets are fragments of a bullet list. Demand structured output, validate it, and treat a malformed response as a retry rather than as data. Done when: Every stored asset lists the brief fields it used and each named field exists in the approved brief. A malformed model response is discarded with the reason logged and never partially stored, tested with a crafted response. Regenerating one asset leaves the others untouched. The model name and prompt version are recorded on the campaign so a result can be explained later. Do not build yet: No validation checks, approval or export yet. ### Phase 4 · Checks written in code, because the model cannot count Build: Write validators in code. For search advertisements, Google's own specification currently allows one to fifteen headlines of up to thirty characters, one to four descriptions of up to ninety characters and a final URL of up to two thousand and forty eight characters: read the current page, put the numbers in configuration with the date you checked, and fail an asset that exceeds them. Flag any number or named entity absent from the facts table, and any asset with no call to action. Trap: Asking the model to keep headlines under thirty characters and trusting the answer produces a rejected upload every time, because a model predicting text is not counting it, and the limits differ per channel besides. Count in code against limits you read from the platform's documentation and stored with the date you read them. Done when: An asset one character over a configured limit is flagged with the measured length and the limit shown. A number absent from the facts table is flagged with the phrase highlighted, tested with a crafted asset. An asset with a blocking finding cannot be marked approved, enforced at the database write and tested through the route. The configuration records each limit with the date and the source URL it came from. Do not build yet: No export or spend controls yet. ### Phase 5 · Approval, export and a ceiling on what a run can cost Build: Export the approved package as Markdown and JSON including findings, fields used, model name and the brief it came from. Estimate token cost before a run and refuse to start above a configured ceiling. Keep a running total per day. Store fetched page text and keys locally and bind the server to localhost. Trap: A retry loop around a validation failure will happily regenerate a campaign forty times overnight against a metered API, and the first sign is the bill. Cap retries, estimate before you call, refuse above a daily ceiling, and show the running total on the page where the button is. Done when: Export produces Markdown and JSON containing every approved asset with its findings, fields used and model name, and the JSON reimports into the app to reconstruct the campaign. A run whose estimate exceeds the ceiling is refused before any API call, tested. The daily spend estimate is visible beside the generate button. The server refuses connections from anything but localhost, verified. Do not build yet: Nothing after this. ### Out of scope (and why) - Image and video generation, which multiplies the cost per run by a large factor and adds a rights question to every asset. - Publishing and scheduling to advertising and social platforms, each of which is an approval process and an integration project of its own. - Multi-model review, where a second model grades the first, because agreement between two models is not evidence about the world. - Any claim about expected performance, click rates or conversion, since nothing in this tool has ever seen a result. ### README must contain - That nothing generates until a person has approved the brief, and that editing it clears approval - That every number and named customer must exist in the facts table with an evidence link - That character limits are counted in code, where the limits came from, and the date they were checked - The fetching etiquette in use: same domain, page cap, declared user agent, delay and cache - The cost ceiling, how the estimate is made, and that the server binds to localhost ===== ARCHITECTURE.md ===== # Architecture · MarketMaster ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Node.js 22 with Express and server-rendered HTML on localhost | no accounts, no build step, nothing to operate | | Storage | better-sqlite3, briefs and campaigns with their approval state | the approval state is the product, so it belongs in a transaction | | Fetching | Same-domain HTML only, a page cap, a declared user agent, cached to disk | one polite pass over one site, not a crawler | | Model | One provider chosen in the environment, structured output validated on arrival | prose in, structured fields out, or discard and retry | | Checks | Deterministic validators in code, run on every asset every time | counting characters is not a job for a language model | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Fetcher | Same-domain fetching, caps, etiquette and cache. | Any HTTP client, provided the cap and the declared agent survive. | | Brief | Extraction with sources and the approval gate. | Any extractor, provided approval stays a required human step. | | Generator | Structured assets with field references. | Any model behind the same schema, with the name recorded. | | Checks | Deterministic limits, claims and call to action rules. | Not swappable for model self-assessment, which is the trap. | | Export | Markdown and JSON with provenance. | Any format, provided findings and fields used travel with the asset. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `PORT` · required · Localhost only. The app is a tool, not a service. - `DATABASE_PATH` · required · Briefs, facts, campaigns, assets and findings. - `MODEL_PROVIDER` · required · Chosen once. Recorded on every campaign. - `MODEL_API_KEY` · required, secret · Never logged, never written into an export. - `FETCH_PAGE_CAP` · required · Maximum pages fetched per site, per run. - `FETCH_USER_AGENT` · required · Identifies you to the site you are fetching from. - `DAILY_SPEND_CEILING` · required · Estimated. A run above it is refused before any API call. - `MAX_RETRIES_PER_ASSET` · required · The other half of not spending a fortune overnight. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · MarketMaster product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Node.js 22 with Express and server-rendered HTML on localhost, better-sqlite3, briefs and campaigns with their approval state, Same-domain HTML only, a page cap, a declared user agent, cached to disk, One provider chosen in the environment, structured output validated on arrival, Deterministic validators in code, run on every asset every time. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Following every link on the page turns a brief extractor into an unannounced crawler that hits a small site a thousand times in a minute, and the first you hear of it is a blocked address. Cap the page count, sleep between requests, identify yourself, and cache so the second run is free. - Letting generation run straight from extraction means a marketing page's own puffery becomes your source of truth, and a phrase the site's designer wrote as filler is now the proof point in three advertisements. Make approval a required step with the source sentence beside each field, so keeping a claim is a decision somebody made. - Accepting free prose and parsing headlines out of it with a regular expression works until the model returns four headlines under a heading you did not ask for, and now half your assets are fragments of a bullet list. Demand structured output, validate it, and treat a malformed response as a retry rather than as data. - Asking the model to keep headlines under thirty characters and trusting the answer produces a rejected upload every time, because a model predicting text is not counting it, and the limits differ per channel besides. Count in code against limits you read from the platform's documentation and stored with the date you read them. - A retry loop around a validation failure will happily regenerate a campaign forty times overnight against a metered API, and the first sign is the bill. Cap retries, estimate before you call, refuse above a daily ceiling, and show the running total on the page where the button is. ===== MILESTONES.md ===== # Delivery milestones · MarketMaster Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · One polite pass over one website Fetching is bounded, identifiable, cached and stops at the domain you were given. ### Steps 1. Cap pages and stay on the domain Two lines of code and the difference between a tool and a nuisance. Files: `src/fetch/crawl.js`, `test/crawl.test.js` 2. Identify yourself and pause between requests Somebody is paying for the bandwidth you are using. Files: `src/fetch/client.js` 3. Cache to disk and prove the rerun is free You will rerun this twenty times while tuning the prompt. Files: `src/fetch/cache.js` ### Done when - [ ] No fetch leaves the given domain and the cap holds - [ ] The user agent names the tool - [ ] A rerun issues no network requests - [ ] Extracted text excludes scripts, styles and navigation ### Watch out - Following every link on the page turns a brief extractor into an unannounced crawler that hits a small site a thousand times in a minute, and the first you hear of it is a blocked address. Cap the page count, sleep between requests, identify yourself, and cache so the second run is free. ## M2 · A brief a person approved, field by field Extraction proposes, a human disposes, and nothing generates before approval. ### Steps 1. Carry the source sentence with every field Approval without the evidence in front of you is a rubber stamp. Files: `src/extract/fields.js` 2. Refuse generation before approval Checked in the route, not in the interface. Files: `src/routes/campaign.js` 3. Turn approved claims into facts rows The next phase checks against this table and nothing else. Files: `src/db/facts.js` ### Done when - [ ] Generation is refused without approval - [ ] Every field shows its source URL and sentence - [ ] Facts require an evidence link - [ ] Editing a brief clears its approval ### Watch out - Letting generation run straight from extraction means a marketing page's own puffery becomes your source of truth, and a phrase the site's designer wrote as filler is now the proof point in three advertisements. Make approval a required step with the source sentence beside each field, so keeping a claim is a decision somebody made. ## M3 · Assets that say where each line came from Every generated asset records the brief fields it used. ### Steps 1. Demand structured assets with field references The reference is what makes the next phase's claim check possible. Files: `src/generate/assets.js` 2. Validate and retry, never salvage A half-parsed advertisement is worse than no advertisement. Files: `src/generate/validate.js`, `test/generate.test.js` 3. Record model and prompt version In three weeks you will not remember which prompt produced this. Files: `src/db/campaigns.js` ### Done when - [ ] Each asset names brief fields that actually exist - [ ] Malformed responses are discarded and retried - [ ] Regenerating one asset leaves others untouched - [ ] Model and prompt version are recorded ### Watch out - Accepting free prose and parsing headlines out of it with a regular expression works until the model returns four headlines under a heading you did not ask for, and now half your assets are fragments of a bullet list. Demand structured output, validate it, and treat a malformed response as a retry rather than as data. ## M4 · Checks written in code, because the model cannot count Length, claims and calls to action are validated deterministically against published limits. ### Steps 1. Count characters in code, per channel The model's opinion about its own length is not evidence. Files: `src/checks/limits.js`, `test/limits.test.js` 2. Flag every number not in the facts table Highlighted in the asset, not summarised in a log. Files: `src/checks/claims.js` 3. Block approval at the write A rule enforced only in the template is a suggestion. Files: `src/db/assets.js` ### Done when - [ ] Over-length assets are flagged with measured length - [ ] Unsupported numbers are flagged in place - [ ] Blocking findings prevent approval at the database layer - [ ] Limits carry their source URL and check date ### Watch out - Asking the model to keep headlines under thirty characters and trusting the answer produces a rejected upload every time, because a model predicting text is not counting it, and the limits differ per channel besides. Count in code against limits you read from the platform's documentation and stored with the date you read them. ## M5 · Approval, export and a ceiling on what a run can cost (production only) An approved package leaves the app as files, and no run can quietly spend a fortune. ### Steps 1. Export with provenance attached An approved asset without its brief is just text. Files: `src/export/package.js` 2. Estimate and cap before calling A retry loop plus a metered API is a bad night. Files: `src/generate/budget.js` 3. Bind to localhost and keep keys in the environment This is a tool on your machine, not a service. Files: `src/server.js` ### Done when - [ ] Exports carry findings, fields used and model name - [ ] The JSON export reimports successfully - [ ] Runs above the ceiling are refused before any call - [ ] The server binds to localhost only ### Watch out - A retry loop around a validation failure will happily regenerate a campaign forty times overnight against a metered API, and the first sign is the bill. Cap retries, estimate before you call, refuse above a daily ceiling, and show the running total on the page where the button is. ===== OPERATIONS.md ===== # Operations · MarketMaster ## Backup The database and the fetch cache. The briefs and facts are the expensive part to recreate, since they carry human approval decisions that nobody wants to make twice. ## Restore Restore and confirm that an exported JSON package reimports to the same campaign, which is the check that the export is a real format rather than a printout. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Estimated spend today against the ceiling, assets sitting flagged, and the age of the character limits in configuration, which is the thing most likely to be quietly out of date. ## Incident checklist An approved asset that turns out to contain an invented claim is the incident worth rehearsing. The fields used column tells you which brief field allowed it, and the fix is usually a missing evidence link rather than a prompt change. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Fetching is confirmed to stay on the domain and inside the cap - [ ] Generation is confirmed refused before brief approval - [ ] An over-length asset is confirmed flagged with its measured length - [ ] A blocking finding is confirmed to prevent approval at the database layer - [ ] A run above the spend ceiling is confirmed refused before any API call ## Launch constraint Do not market omitted MarketMaster capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Localhost only. The app is a tool, not a service. PORT=4860 # Required. Briefs, facts, campaigns, assets and findings. DATABASE_PATH=./data/campaigns.db # Required. Chosen once. Recorded on every campaign. MODEL_PROVIDER=one provider name # Required · secret. Never logged, never written into an export. MODEL_API_KEY=sk-...redacted # Required. Maximum pages fetched per site, per run. FETCH_PAGE_CAP=25 # Required. Identifies you to the site you are fetching from. FETCH_USER_AGENT=campaign-brief-tool/1.0 (contact: you@example.com) # Required. Estimated. A run above it is refused before any API call. DAILY_SPEND_CEILING=2.00 # Required. The other half of not spending a fortune overnight. MAX_RETRIES_PER_ASSET=3
# MarketMaster · indie build A campaign generator whose hard problems are extracting a brief from one website without becoming a crawler, keeping every generated line traceable to an approved fact, and enforcing platform limits in code because the model cannot count. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Node.js 22 with Express and server-rendered HTML on localhost | no accounts, no build step, nothing to operate | | Storage | better-sqlite3, briefs and campaigns with their approval state | the approval state is the product, so it belongs in a transaction | | Fetching | Same-domain HTML only, a page cap, a declared user agent, cached to disk | one polite pass over one site, not a crawler | | Model | One provider chosen in the environment, structured output validated on arrival | prose in, structured fields out, or discard and retry | | Checks | Deterministic validators in code, run on every asset every time | counting characters is not a job for a language model | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the server and the generation worker. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **An API key for one text model** · cents per campaign at current small-model prices, but set the cap anyway - Why: Generation needs exactly one provider, and it should carry a hard spending cap because a retry loop is the usual way a bill happens. - Get it: Create a key with your chosen provider and set a monthly limit on the account. - Verify: A campaign generates and the estimated cost is recorded - [ ] **The current character limits for each channel you target** · free - Why: These are the checks the whole build exists to run, and a limit you remembered from last year is the reason an upload gets rejected. - Get it: Read each platform's own specification page and copy the numbers into configuration with the date and the URL. - Verify: Configuration holds every limit with its source URL and check date - [ ] **A website you are allowed to extract from** · free - Why: Your own site or a client's, with permission, because the tool fetches real pages and stores their text on your machine. - Get it: Use your own domain, or get written permission for a client's. - Verify: A brief extracts from it inside the page cap - [ ] **Your daily spend ceiling and retry cap** · free - Why: Both are enforced in code, and choosing them after an expensive night is choosing them too late. - Get it: Write down the maximum estimated spend per day and the maximum retries per asset. - Verify: Both are in configuration and named in tests ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Image and video generation. - Publishing and scheduling integrations. - Multi-model review. - Any predicted performance figure. - multi-model campaign review - generated image and video assets - live social and email integrations - scheduled publishing and delivery history - maintained ad-pattern and platform-limit references If one of those is essential to you, that is the reason to keep paying for MarketMaster, and the README should say so rather than pretend.
# Build brief · MarketMaster The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a campaign copy generator that will not emit a claim the approved brief does not support to replace MarketMaster. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero One rule, and it is the reason this is worth building rather than pasting a prompt into a chat window. Every sentence the generator produces must be traceable to a field a human approved. A language model asked to write an advertisement will produce social proof, growth figures and awards because that is what advertisements contain, and the invented ones read exactly like the real ones. So the flow is: extract, show the brief, let a person edit and approve it, then generate only from approved fields, and record on every asset which fields it drew on. Any number, superlative or named customer that is not in the brief is flagged beside the asset and the asset cannot be marked approved until it is resolved. The second half of that rule: checks that matter are written in code, not asked of the model. Character limits, missing calls to action and unsupported figures are arithmetic and string matching. A model asked whether its own headline fits in thirty characters will tell you yes. ### Stack (fixed, do not substitute) - Runtime: Node.js 22 with Express and server-rendered HTML on localhost · no accounts, no build step, nothing to operate - Storage: better-sqlite3, briefs and campaigns with their approval state · the approval state is the product, so it belongs in a transaction - Fetching: Same-domain HTML only, a page cap, a declared user agent, cached to disk · one polite pass over one site, not a crawler - Model: One provider chosen in the environment, structured output validated on arrival · prose in, structured fields out, or discard and retry - Checks: Deterministic validators in code, run on every asset every time · counting characters is not a job for a language model ### Data model (create this before Phase 1) - sources(id, site_root, url, fetched_at, status, bytes, cache_path) · one row per page actually fetched - briefs(id, site_root, fields_json, approved_at, approved_by_note) · nothing generates until approved_at is set - facts(brief_id, key, value, evidence_url) · the only numbers and named customers allowed to appear - campaigns(id, brief_id, model_name, prompt_version, created_at, token_cost_estimate) - assets(id, campaign_id, channel, kind, text, fields_used_json, state) · state is draft, flagged or approved - findings(id, asset_id, rule, detail, severity) · one row per failed check, cleared only by an edit - An asset with an open finding of blocking severity cannot reach approved, enforced in the database write rather than in the template. ### Phase 1 · One polite pass over one website Build: Accept one site root. Fetch its HTML pages on the same domain only, up to a page cap, with a declared user agent naming the tool and a delay between requests. Honour the site's crawl directives and any noindex it declares. Cache every response to disk so a rerun fetches nothing, and extract visible text only, dropping navigation, scripts and styles. Trap: Following every link on the page turns a brief extractor into an unannounced crawler that hits a small site a thousand times in a minute, and the first you hear of it is a blocked address. Cap the page count, sleep between requests, identify yourself, and cache so the second run is free. Done when: A run fetches no more than the configured page cap and no URL outside the given domain, tested with a fixture site containing external and subdomain links. The user agent string identifies the tool and appears in the README. A second run over the same site issues no network requests, verified by counting them. Extracted text excludes script, style and navigation content, checked against a fixture. Do not build yet: No brief, no generation, no checks or exports. ### Phase 2 · A brief a person approved, field by field Build: Extract proposed values for product, audience, pricing, proof points, brand voice and call to action, each with the source URL and the exact sentence it came from. Present them in an editable form. Approval writes approved_at and freezes the fields. Every number or named customer the person keeps becomes a row in the facts table with its evidence link. Trap: Letting generation run straight from extraction means a marketing page's own puffery becomes your source of truth, and a phrase the site's designer wrote as filler is now the proof point in three advertisements. Make approval a required step with the source sentence beside each field, so keeping a claim is a decision somebody made. Done when: Generation is refused for a brief with no approved_at, tested through the route. Every proposed field displays its source URL and source sentence. Approved numbers and named customers appear in the facts table with an evidence link, and a fact without evidence cannot be saved. Editing an approved brief clears the approval and requires a new one. Do not build yet: No asset generation, checks or exports yet. ### Phase 3 · Assets that say where each line came from Build: Generate a fixed set per campaign: three advertisement concepts, five social posts, a five-slide carousel outline, a short video script and one email. Require the model to return structured assets naming the brief fields each one drew on, validate against the schema, and discard and retry a bounded number of times rather than storing anything partial. Trap: Accepting free prose and parsing headlines out of it with a regular expression works until the model returns four headlines under a heading you did not ask for, and now half your assets are fragments of a bullet list. Demand structured output, validate it, and treat a malformed response as a retry rather than as data. Done when: Every stored asset lists the brief fields it used and each named field exists in the approved brief. A malformed model response is discarded with the reason logged and never partially stored, tested with a crafted response. Regenerating one asset leaves the others untouched. The model name and prompt version are recorded on the campaign so a result can be explained later. Do not build yet: No validation checks, approval or export yet. ### Phase 4 · Checks written in code, because the model cannot count Build: Write validators in code. For search advertisements, Google's own specification currently allows one to fifteen headlines of up to thirty characters, one to four descriptions of up to ninety characters and a final URL of up to two thousand and forty eight characters: read the current page, put the numbers in configuration with the date you checked, and fail an asset that exceeds them. Flag any number or named entity absent from the facts table, and any asset with no call to action. Trap: Asking the model to keep headlines under thirty characters and trusting the answer produces a rejected upload every time, because a model predicting text is not counting it, and the limits differ per channel besides. Count in code against limits you read from the platform's documentation and stored with the date you read them. Done when: An asset one character over a configured limit is flagged with the measured length and the limit shown. A number absent from the facts table is flagged with the phrase highlighted, tested with a crafted asset. An asset with a blocking finding cannot be marked approved, enforced at the database write and tested through the route. The configuration records each limit with the date and the source URL it came from. Do not build yet: No export or spend controls yet. ### Phase 5 · Approval, export and a ceiling on what a run can cost Build: Export the approved package as Markdown and JSON including findings, fields used, model name and the brief it came from. Estimate token cost before a run and refuse to start above a configured ceiling. Keep a running total per day. Store fetched page text and keys locally and bind the server to localhost. Trap: A retry loop around a validation failure will happily regenerate a campaign forty times overnight against a metered API, and the first sign is the bill. Cap retries, estimate before you call, refuse above a daily ceiling, and show the running total on the page where the button is. Done when: Export produces Markdown and JSON containing every approved asset with its findings, fields used and model name, and the JSON reimports into the app to reconstruct the campaign. A run whose estimate exceeds the ceiling is refused before any API call, tested. The daily spend estimate is visible beside the generate button. The server refuses connections from anything but localhost, verified. Do not build yet: Nothing after this. ### Out of scope (and why) - Image and video generation, which multiplies the cost per run by a large factor and adds a rights question to every asset. - Publishing and scheduling to advertising and social platforms, each of which is an approval process and an integration project of its own. - Multi-model review, where a second model grades the first, because agreement between two models is not evidence about the world. - Any claim about expected performance, click rates or conversion, since nothing in this tool has ever seen a result. ### README must contain - That nothing generates until a person has approved the brief, and that editing it clears approval - That every number and named customer must exist in the facts table with an evidence link - That character limits are counted in code, where the limits came from, and the date they were checked - The fetching etiquette in use: same domain, page cap, declared user agent, delay and cache - The cost ceiling, how the estimate is made, and that the server binds to localhost
# Agent instructions · MarketMaster indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Node.js 22 with Express and server-rendered HTML on localhost, better-sqlite3, briefs and campaigns with their approval state, Same-domain HTML only, a page cap, a declared user agent, cached to disk, One provider chosen in the environment, structured output validated on arrival, Deterministic validators in code, run on every asset every time. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Following every link on the page turns a brief extractor into an unannounced crawler that hits a small site a thousand times in a minute, and the first you hear of it is a blocked address. Cap the page count, sleep between requests, identify yourself, and cache so the second run is free. - Letting generation run straight from extraction means a marketing page's own puffery becomes your source of truth, and a phrase the site's designer wrote as filler is now the proof point in three advertisements. Make approval a required step with the source sentence beside each field, so keeping a claim is a decision somebody made. - Accepting free prose and parsing headlines out of it with a regular expression works until the model returns four headlines under a heading you did not ask for, and now half your assets are fragments of a bullet list. Demand structured output, validate it, and treat a malformed response as a retry rather than as data. - Asking the model to keep headlines under thirty characters and trusting the answer produces a rejected upload every time, because a model predicting text is not counting it, and the limits differ per channel besides. Count in code against limits you read from the platform's documentation and stored with the date you read them.
# Build plan · MarketMaster A campaign generator whose hard problems are extracting a brief from one website without becoming a crawler, keeping every generated line traceable to an approved fact, and enforcing platform limits in code because the model cannot count. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · One polite pass over one website Fetching is bounded, identifiable, cached and stops at the domain you were given. ### Steps 1. Cap pages and stay on the domain Two lines of code and the difference between a tool and a nuisance. Files: `src/fetch/crawl.js`, `test/crawl.test.js` 2. Identify yourself and pause between requests Somebody is paying for the bandwidth you are using. Files: `src/fetch/client.js` 3. Cache to disk and prove the rerun is free You will rerun this twenty times while tuning the prompt. Files: `src/fetch/cache.js` ### Done when - [ ] No fetch leaves the given domain and the cap holds - [ ] The user agent names the tool - [ ] A rerun issues no network requests - [ ] Extracted text excludes scripts, styles and navigation ### Watch out - Following every link on the page turns a brief extractor into an unannounced crawler that hits a small site a thousand times in a minute, and the first you hear of it is a blocked address. Cap the page count, sleep between requests, identify yourself, and cache so the second run is free. ## Phase 2 · A brief a person approved, field by field Extraction proposes, a human disposes, and nothing generates before approval. ### Steps 1. Carry the source sentence with every field Approval without the evidence in front of you is a rubber stamp. Files: `src/extract/fields.js` 2. Refuse generation before approval Checked in the route, not in the interface. Files: `src/routes/campaign.js` 3. Turn approved claims into facts rows The next phase checks against this table and nothing else. Files: `src/db/facts.js` ### Done when - [ ] Generation is refused without approval - [ ] Every field shows its source URL and sentence - [ ] Facts require an evidence link - [ ] Editing a brief clears its approval ### Watch out - Letting generation run straight from extraction means a marketing page's own puffery becomes your source of truth, and a phrase the site's designer wrote as filler is now the proof point in three advertisements. Make approval a required step with the source sentence beside each field, so keeping a claim is a decision somebody made. ## Phase 3 · Assets that say where each line came from Every generated asset records the brief fields it used. ### Steps 1. Demand structured assets with field references The reference is what makes the next phase's claim check possible. Files: `src/generate/assets.js` 2. Validate and retry, never salvage A half-parsed advertisement is worse than no advertisement. Files: `src/generate/validate.js`, `test/generate.test.js` 3. Record model and prompt version In three weeks you will not remember which prompt produced this. Files: `src/db/campaigns.js` ### Done when - [ ] Each asset names brief fields that actually exist - [ ] Malformed responses are discarded and retried - [ ] Regenerating one asset leaves others untouched - [ ] Model and prompt version are recorded ### Watch out - Accepting free prose and parsing headlines out of it with a regular expression works until the model returns four headlines under a heading you did not ask for, and now half your assets are fragments of a bullet list. Demand structured output, validate it, and treat a malformed response as a retry rather than as data. ## Phase 4 · Checks written in code, because the model cannot count Length, claims and calls to action are validated deterministically against published limits. ### Steps 1. Count characters in code, per channel The model's opinion about its own length is not evidence. Files: `src/checks/limits.js`, `test/limits.test.js` 2. Flag every number not in the facts table Highlighted in the asset, not summarised in a log. Files: `src/checks/claims.js` 3. Block approval at the write A rule enforced only in the template is a suggestion. Files: `src/db/assets.js` ### Done when - [ ] Over-length assets are flagged with measured length - [ ] Unsupported numbers are flagged in place - [ ] Blocking findings prevent approval at the database layer - [ ] Limits carry their source URL and check date ### Watch out - Asking the model to keep headlines under thirty characters and trusting the answer produces a rejected upload every time, because a model predicting text is not counting it, and the limits differ per channel besides. Count in code against limits you read from the platform's documentation and stored with the date you read them. ## Not in this build - Image and video generation. - Publishing and scheduling integrations. - Multi-model review. - Any predicted performance figure. ## After v1, if you want it - A second channel's limits added as configuration rather than code, once the first set has been checked twice - A diff view between two campaigns from the same brief, which makes prompt changes visible - A brief that merges several approved sources, once single-source extraction has been used in anger
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Localhost only. The app is a tool, not a service. PORT=4860 # Required. Briefs, facts, campaigns, assets and findings. DATABASE_PATH=./data/campaigns.db # Required. Chosen once. Recorded on every campaign. MODEL_PROVIDER=one provider name # Required · secret. Never logged, never written into an export. MODEL_API_KEY=sk-...redacted # Required. Maximum pages fetched per site, per run. FETCH_PAGE_CAP=25 # Required. Identifies you to the site you are fetching from. FETCH_USER_AGENT=campaign-brief-tool/1.0 (contact: you@example.com) # Required. Estimated. A run above it is refused before any API call. DAILY_SPEND_CEILING=2.00 # Required. The other half of not spending a fortune overnight. MAX_RETRIES_PER_ASSET=3
# MarketMaster · product brief ## Problem A solo campaign generator that scrapes one site and prompts an LLM for coordinated copy is a realistic weekend build. MarketMaster's paid value is the deeper research, multi-format media generation, deterministic quality gates, independent review, integrations, scheduling, and maintained publishing workflow. ## Product outcome A generator that reads one site politely, will not write a sentence the approved brief does not support, counts characters in code rather than asking the model, and cannot spend more than you allowed it to. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - LLM API key - website text extraction - SQLite - hosted or local web app ## Explicit non-goals for v1 - Image and video generation. - Publishing and scheduling integrations. - Multi-model review. - Any predicted performance figure. - multi-model campaign review - generated image and video assets - live social and email integrations - scheduled publishing and delivery history - maintained ad-pattern and platform-limit references ## Success criteria - Fetching is confirmed to stay on the domain and inside the cap - Generation is confirmed refused before brief approval - An over-length asset is confirmed flagged with its measured length - A blocking finding is confirmed to prevent approval at the database layer - A run above the spend ceiling is confirmed refused before any API call
# Build brief · MarketMaster The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a campaign copy generator that will not emit a claim the approved brief does not support to replace MarketMaster. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero One rule, and it is the reason this is worth building rather than pasting a prompt into a chat window. Every sentence the generator produces must be traceable to a field a human approved. A language model asked to write an advertisement will produce social proof, growth figures and awards because that is what advertisements contain, and the invented ones read exactly like the real ones. So the flow is: extract, show the brief, let a person edit and approve it, then generate only from approved fields, and record on every asset which fields it drew on. Any number, superlative or named customer that is not in the brief is flagged beside the asset and the asset cannot be marked approved until it is resolved. The second half of that rule: checks that matter are written in code, not asked of the model. Character limits, missing calls to action and unsupported figures are arithmetic and string matching. A model asked whether its own headline fits in thirty characters will tell you yes. ### Stack (fixed, do not substitute) - Runtime: Node.js 22 with Express and server-rendered HTML on localhost · no accounts, no build step, nothing to operate - Storage: better-sqlite3, briefs and campaigns with their approval state · the approval state is the product, so it belongs in a transaction - Fetching: Same-domain HTML only, a page cap, a declared user agent, cached to disk · one polite pass over one site, not a crawler - Model: One provider chosen in the environment, structured output validated on arrival · prose in, structured fields out, or discard and retry - Checks: Deterministic validators in code, run on every asset every time · counting characters is not a job for a language model ### Data model (create this before Phase 1) - sources(id, site_root, url, fetched_at, status, bytes, cache_path) · one row per page actually fetched - briefs(id, site_root, fields_json, approved_at, approved_by_note) · nothing generates until approved_at is set - facts(brief_id, key, value, evidence_url) · the only numbers and named customers allowed to appear - campaigns(id, brief_id, model_name, prompt_version, created_at, token_cost_estimate) - assets(id, campaign_id, channel, kind, text, fields_used_json, state) · state is draft, flagged or approved - findings(id, asset_id, rule, detail, severity) · one row per failed check, cleared only by an edit - An asset with an open finding of blocking severity cannot reach approved, enforced in the database write rather than in the template. ### Phase 1 · One polite pass over one website Build: Accept one site root. Fetch its HTML pages on the same domain only, up to a page cap, with a declared user agent naming the tool and a delay between requests. Honour the site's crawl directives and any noindex it declares. Cache every response to disk so a rerun fetches nothing, and extract visible text only, dropping navigation, scripts and styles. Trap: Following every link on the page turns a brief extractor into an unannounced crawler that hits a small site a thousand times in a minute, and the first you hear of it is a blocked address. Cap the page count, sleep between requests, identify yourself, and cache so the second run is free. Done when: A run fetches no more than the configured page cap and no URL outside the given domain, tested with a fixture site containing external and subdomain links. The user agent string identifies the tool and appears in the README. A second run over the same site issues no network requests, verified by counting them. Extracted text excludes script, style and navigation content, checked against a fixture. Do not build yet: No brief, no generation, no checks or exports. ### Phase 2 · A brief a person approved, field by field Build: Extract proposed values for product, audience, pricing, proof points, brand voice and call to action, each with the source URL and the exact sentence it came from. Present them in an editable form. Approval writes approved_at and freezes the fields. Every number or named customer the person keeps becomes a row in the facts table with its evidence link. Trap: Letting generation run straight from extraction means a marketing page's own puffery becomes your source of truth, and a phrase the site's designer wrote as filler is now the proof point in three advertisements. Make approval a required step with the source sentence beside each field, so keeping a claim is a decision somebody made. Done when: Generation is refused for a brief with no approved_at, tested through the route. Every proposed field displays its source URL and source sentence. Approved numbers and named customers appear in the facts table with an evidence link, and a fact without evidence cannot be saved. Editing an approved brief clears the approval and requires a new one. Do not build yet: No asset generation, checks or exports yet. ### Phase 3 · Assets that say where each line came from Build: Generate a fixed set per campaign: three advertisement concepts, five social posts, a five-slide carousel outline, a short video script and one email. Require the model to return structured assets naming the brief fields each one drew on, validate against the schema, and discard and retry a bounded number of times rather than storing anything partial. Trap: Accepting free prose and parsing headlines out of it with a regular expression works until the model returns four headlines under a heading you did not ask for, and now half your assets are fragments of a bullet list. Demand structured output, validate it, and treat a malformed response as a retry rather than as data. Done when: Every stored asset lists the brief fields it used and each named field exists in the approved brief. A malformed model response is discarded with the reason logged and never partially stored, tested with a crafted response. Regenerating one asset leaves the others untouched. The model name and prompt version are recorded on the campaign so a result can be explained later. Do not build yet: No validation checks, approval or export yet. ### Phase 4 · Checks written in code, because the model cannot count Build: Write validators in code. For search advertisements, Google's own specification currently allows one to fifteen headlines of up to thirty characters, one to four descriptions of up to ninety characters and a final URL of up to two thousand and forty eight characters: read the current page, put the numbers in configuration with the date you checked, and fail an asset that exceeds them. Flag any number or named entity absent from the facts table, and any asset with no call to action. Trap: Asking the model to keep headlines under thirty characters and trusting the answer produces a rejected upload every time, because a model predicting text is not counting it, and the limits differ per channel besides. Count in code against limits you read from the platform's documentation and stored with the date you read them. Done when: An asset one character over a configured limit is flagged with the measured length and the limit shown. A number absent from the facts table is flagged with the phrase highlighted, tested with a crafted asset. An asset with a blocking finding cannot be marked approved, enforced at the database write and tested through the route. The configuration records each limit with the date and the source URL it came from. Do not build yet: No export or spend controls yet. ### Phase 5 · Approval, export and a ceiling on what a run can cost Build: Export the approved package as Markdown and JSON including findings, fields used, model name and the brief it came from. Estimate token cost before a run and refuse to start above a configured ceiling. Keep a running total per day. Store fetched page text and keys locally and bind the server to localhost. Trap: A retry loop around a validation failure will happily regenerate a campaign forty times overnight against a metered API, and the first sign is the bill. Cap retries, estimate before you call, refuse above a daily ceiling, and show the running total on the page where the button is. Done when: Export produces Markdown and JSON containing every approved asset with its findings, fields used and model name, and the JSON reimports into the app to reconstruct the campaign. A run whose estimate exceeds the ceiling is refused before any API call, tested. The daily spend estimate is visible beside the generate button. The server refuses connections from anything but localhost, verified. Do not build yet: Nothing after this. ### Out of scope (and why) - Image and video generation, which multiplies the cost per run by a large factor and adds a rights question to every asset. - Publishing and scheduling to advertising and social platforms, each of which is an approval process and an integration project of its own. - Multi-model review, where a second model grades the first, because agreement between two models is not evidence about the world. - Any claim about expected performance, click rates or conversion, since nothing in this tool has ever seen a result. ### README must contain - That nothing generates until a person has approved the brief, and that editing it clears approval - That every number and named customer must exist in the facts table with an evidence link - That character limits are counted in code, where the limits came from, and the date they were checked - The fetching etiquette in use: same domain, page cap, declared user agent, delay and cache - The cost ceiling, how the estimate is made, and that the server binds to localhost
# Architecture · MarketMaster ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Node.js 22 with Express and server-rendered HTML on localhost | no accounts, no build step, nothing to operate | | Storage | better-sqlite3, briefs and campaigns with their approval state | the approval state is the product, so it belongs in a transaction | | Fetching | Same-domain HTML only, a page cap, a declared user agent, cached to disk | one polite pass over one site, not a crawler | | Model | One provider chosen in the environment, structured output validated on arrival | prose in, structured fields out, or discard and retry | | Checks | Deterministic validators in code, run on every asset every time | counting characters is not a job for a language model | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Fetcher | Same-domain fetching, caps, etiquette and cache. | Any HTTP client, provided the cap and the declared agent survive. | | Brief | Extraction with sources and the approval gate. | Any extractor, provided approval stays a required human step. | | Generator | Structured assets with field references. | Any model behind the same schema, with the name recorded. | | Checks | Deterministic limits, claims and call to action rules. | Not swappable for model self-assessment, which is the trap. | | Export | Markdown and JSON with provenance. | Any format, provided findings and fields used travel with the asset. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `PORT` · required · Localhost only. The app is a tool, not a service. - `DATABASE_PATH` · required · Briefs, facts, campaigns, assets and findings. - `MODEL_PROVIDER` · required · Chosen once. Recorded on every campaign. - `MODEL_API_KEY` · required, secret · Never logged, never written into an export. - `FETCH_PAGE_CAP` · required · Maximum pages fetched per site, per run. - `FETCH_USER_AGENT` · required · Identifies you to the site you are fetching from. - `DAILY_SPEND_CEILING` · required · Estimated. A run above it is refused before any API call. - `MAX_RETRIES_PER_ASSET` · required · The other half of not spending a fortune overnight. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · MarketMaster product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Node.js 22 with Express and server-rendered HTML on localhost, better-sqlite3, briefs and campaigns with their approval state, Same-domain HTML only, a page cap, a declared user agent, cached to disk, One provider chosen in the environment, structured output validated on arrival, Deterministic validators in code, run on every asset every time. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Following every link on the page turns a brief extractor into an unannounced crawler that hits a small site a thousand times in a minute, and the first you hear of it is a blocked address. Cap the page count, sleep between requests, identify yourself, and cache so the second run is free. - Letting generation run straight from extraction means a marketing page's own puffery becomes your source of truth, and a phrase the site's designer wrote as filler is now the proof point in three advertisements. Make approval a required step with the source sentence beside each field, so keeping a claim is a decision somebody made. - Accepting free prose and parsing headlines out of it with a regular expression works until the model returns four headlines under a heading you did not ask for, and now half your assets are fragments of a bullet list. Demand structured output, validate it, and treat a malformed response as a retry rather than as data. - Asking the model to keep headlines under thirty characters and trusting the answer produces a rejected upload every time, because a model predicting text is not counting it, and the limits differ per channel besides. Count in code against limits you read from the platform's documentation and stored with the date you read them. - A retry loop around a validation failure will happily regenerate a campaign forty times overnight against a metered API, and the first sign is the bill. Cap retries, estimate before you call, refuse above a daily ceiling, and show the running total on the page where the button is.
# Delivery milestones · MarketMaster Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · One polite pass over one website Fetching is bounded, identifiable, cached and stops at the domain you were given. ### Steps 1. Cap pages and stay on the domain Two lines of code and the difference between a tool and a nuisance. Files: `src/fetch/crawl.js`, `test/crawl.test.js` 2. Identify yourself and pause between requests Somebody is paying for the bandwidth you are using. Files: `src/fetch/client.js` 3. Cache to disk and prove the rerun is free You will rerun this twenty times while tuning the prompt. Files: `src/fetch/cache.js` ### Done when - [ ] No fetch leaves the given domain and the cap holds - [ ] The user agent names the tool - [ ] A rerun issues no network requests - [ ] Extracted text excludes scripts, styles and navigation ### Watch out - Following every link on the page turns a brief extractor into an unannounced crawler that hits a small site a thousand times in a minute, and the first you hear of it is a blocked address. Cap the page count, sleep between requests, identify yourself, and cache so the second run is free. ## M2 · A brief a person approved, field by field Extraction proposes, a human disposes, and nothing generates before approval. ### Steps 1. Carry the source sentence with every field Approval without the evidence in front of you is a rubber stamp. Files: `src/extract/fields.js` 2. Refuse generation before approval Checked in the route, not in the interface. Files: `src/routes/campaign.js` 3. Turn approved claims into facts rows The next phase checks against this table and nothing else. Files: `src/db/facts.js` ### Done when - [ ] Generation is refused without approval - [ ] Every field shows its source URL and sentence - [ ] Facts require an evidence link - [ ] Editing a brief clears its approval ### Watch out - Letting generation run straight from extraction means a marketing page's own puffery becomes your source of truth, and a phrase the site's designer wrote as filler is now the proof point in three advertisements. Make approval a required step with the source sentence beside each field, so keeping a claim is a decision somebody made. ## M3 · Assets that say where each line came from Every generated asset records the brief fields it used. ### Steps 1. Demand structured assets with field references The reference is what makes the next phase's claim check possible. Files: `src/generate/assets.js` 2. Validate and retry, never salvage A half-parsed advertisement is worse than no advertisement. Files: `src/generate/validate.js`, `test/generate.test.js` 3. Record model and prompt version In three weeks you will not remember which prompt produced this. Files: `src/db/campaigns.js` ### Done when - [ ] Each asset names brief fields that actually exist - [ ] Malformed responses are discarded and retried - [ ] Regenerating one asset leaves others untouched - [ ] Model and prompt version are recorded ### Watch out - Accepting free prose and parsing headlines out of it with a regular expression works until the model returns four headlines under a heading you did not ask for, and now half your assets are fragments of a bullet list. Demand structured output, validate it, and treat a malformed response as a retry rather than as data. ## M4 · Checks written in code, because the model cannot count Length, claims and calls to action are validated deterministically against published limits. ### Steps 1. Count characters in code, per channel The model's opinion about its own length is not evidence. Files: `src/checks/limits.js`, `test/limits.test.js` 2. Flag every number not in the facts table Highlighted in the asset, not summarised in a log. Files: `src/checks/claims.js` 3. Block approval at the write A rule enforced only in the template is a suggestion. Files: `src/db/assets.js` ### Done when - [ ] Over-length assets are flagged with measured length - [ ] Unsupported numbers are flagged in place - [ ] Blocking findings prevent approval at the database layer - [ ] Limits carry their source URL and check date ### Watch out - Asking the model to keep headlines under thirty characters and trusting the answer produces a rejected upload every time, because a model predicting text is not counting it, and the limits differ per channel besides. Count in code against limits you read from the platform's documentation and stored with the date you read them. ## M5 · Approval, export and a ceiling on what a run can cost (production only) An approved package leaves the app as files, and no run can quietly spend a fortune. ### Steps 1. Export with provenance attached An approved asset without its brief is just text. Files: `src/export/package.js` 2. Estimate and cap before calling A retry loop plus a metered API is a bad night. Files: `src/generate/budget.js` 3. Bind to localhost and keep keys in the environment This is a tool on your machine, not a service. Files: `src/server.js` ### Done when - [ ] Exports carry findings, fields used and model name - [ ] The JSON export reimports successfully - [ ] Runs above the ceiling are refused before any call - [ ] The server binds to localhost only ### Watch out - A retry loop around a validation failure will happily regenerate a campaign forty times overnight against a metered API, and the first sign is the bill. Cap retries, estimate before you call, refuse above a daily ceiling, and show the running total on the page where the button is.
# Operations · MarketMaster ## Backup The database and the fetch cache. The briefs and facts are the expensive part to recreate, since they carry human approval decisions that nobody wants to make twice. ## Restore Restore and confirm that an exported JSON package reimports to the same campaign, which is the check that the export is a real format rather than a printout. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Estimated spend today against the ceiling, assets sitting flagged, and the age of the character limits in configuration, which is the thing most likely to be quietly out of date. ## Incident checklist An approved asset that turns out to contain an invented claim is the incident worth rehearsing. The fields used column tells you which brief field allowed it, and the fix is usually a missing evidence link rather than a prompt change. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Fetching is confirmed to stay on the domain and inside the cap - [ ] Generation is confirmed refused before brief approval - [ ] An over-length asset is confirmed flagged with its measured length - [ ] A blocking finding is confirmed to prevent approval at the database layer - [ ] A run above the spend ceiling is confirmed refused before any API call ## Launch constraint Do not market omitted MarketMaster capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Localhost only. The app is a tool, not a service. PORT=4860 # Required. Briefs, facts, campaigns, assets and findings. DATABASE_PATH=./data/campaigns.db # Required. Chosen once. Recorded on every campaign. MODEL_PROVIDER=one provider name # Required · secret. Never logged, never written into an export. MODEL_API_KEY=sk-...redacted # Required. Maximum pages fetched per site, per run. FETCH_PAGE_CAP=25 # Required. Identifies you to the site you are fetching from. FETCH_USER_AGENT=campaign-brief-tool/1.0 (contact: you@example.com) # Required. Estimated. A run above it is refused before any API call. DAILY_SPEND_CEILING=2.00 # Required. The other half of not spending a fortune overnight. MAX_RETRIES_PER_ASSET=3
$ choose a build depth, inspect the files, then open the complete pack in your agent
They pay to move from a website to a coordinated, reviewable, publish-ready campaign without maintaining research, media, validation, integration, and delivery infrastructure themselves.
xmulti-model campaign review
xgenerated image and video assets
xlive social and email integrations
xscheduled publishing and delivery history
xmaintained ad-pattern and platform-limit references
Nothing worth pointing at. That's why the prompt exists.
MarketMaster pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| starter | $29/workspace | $24.17/workspace | 5 campaigns/month. |
| pro | $79/workspace | $65.83/workspace | Unlimited campaigns; 10 recurring automations; 100 images; 20 clips; 8 shorts; 20 voiceovers/month. |
| studio | $199/workspace | $165.83/workspace | Unlimited campaigns and images; 150 clips; 40 shorts; 100 voiceovers/month; 10 recurring automations. |
free tierno free tier
billingmonthly + annual (2 months free); cancel anytime with access through the paid period
hidden costsA current Starter promotion makes the first invoice $0, then renews at $29/month; the published promotion window runs through 2026-08-20.
verified 2026-08-14 · source ↗
Vibecode MarketMaster
Kinda. The core of MarketMaster is buildable in a weekend with the prompt on this page, but there are real gaps: multi-model campaign review, generated image and video assets. Read the honest list above before committing.
How much does MarketMaster cost?
MarketMaster costs about $29/month (Starter, checked 2026-07-30), which is $348 per year.
What do I lose by replacing MarketMaster?
Honestly: multi-model campaign review; generated image and video assets; live social and email integrations; scheduled publishing and delivery history; maintained ad-pattern and platform-limit references. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to MarketMaster?
No mature open-source alternative worth pointing at, which is exactly why the one-shot prompt on this page exists.