Build MarketMaster

KINDAreplaces $29/mosaves $348/yrback to the verdict

0%0 of 17 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

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 weekendthe files for this build are in the project pack

RuntimeNode.js 22 with Express and server-rendered HTML on localhostStoragebetter-sqlite3, briefs and campaigns with their approval stateFetchingSame-domain HTML only, a page cap, a declared user agent, cached to diskModelOne provider chosen in the environment, structured output validated on arrivalChecksDeterministic validators in code, run on every asset every time

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

  1. installfree

    Why Runs the server and the generation worker.

    Get it nodejs.org, or nvm install 22. open ↗

    Verify node --version prints v22 or higher

  2. API keycents 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

  3. have readyfree

    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. open ↗

    Verify Configuration holds every limit with its source URL and check date

  4. have readyfree

    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

  5. decidefree

    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

Data model

Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.

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

Environment variables

These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.

VariableNeededExampleWhere the value comes from
PORTrequired4860Localhost only. The app is a tool, not a service.
DATABASE_PATHrequired./data/campaigns.dbBriefs, facts, campaigns, assets and findings.
MODEL_PROVIDERrequiredone provider nameChosen once. Recorded on every campaign.
MODEL_API_KEYsecretrequiredsk-...redactedNever logged, never written into an export.
FETCH_PAGE_CAPrequired25Maximum pages fetched per site, per run.
FETCH_USER_AGENTrequiredcampaign-brief-tool/1.0 (contact: you@example.com)Identifies you to the site you are fetching from.
DAILY_SPEND_CEILINGrequired2.00Estimated. A run above it is refused before any API call.
MAX_RETRIES_PER_ASSETrequired3The other half of not spending a fortune overnight.

The build, in order

  1. One polite pass over one website

    Fetching is bounded, identifiable, cached and stops at the domain you were given.

    1. Two lines of code and the difference between a tool and a nuisance.

      Files src/fetch/crawl.jstest/crawl.test.js

    2. Somebody is paying for the bandwidth you are using.

      Files src/fetch/client.js

    3. You will rerun this twenty times while tuning the prompt.

      Files src/fetch/cache.js

    done when · tick each as it passes
    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.
  2. A brief a person approved, field by field

    Extraction proposes, a human disposes, and nothing generates before approval.

    1. Approval without the evidence in front of you is a rubber stamp.

      Files src/extract/fields.js

    2. Checked in the route, not in the interface.

      Files src/routes/campaign.js

    3. The next phase checks against this table and nothing else.

      Files src/db/facts.js

    done when · tick each as it passes
    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.
  3. Assets that say where each line came from

    Every generated asset records the brief fields it used.

    1. The reference is what makes the next phase's claim check possible.

      Files src/generate/assets.js

    2. A half-parsed advertisement is worse than no advertisement.

      Files src/generate/validate.jstest/generate.test.js

    3. In three weeks you will not remember which prompt produced this.

      Files src/db/campaigns.js

    done when · tick each as it passes
    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.
  4. Checks written in code, because the model cannot count

    Length, claims and calls to action are validated deterministically against published limits.

    1. The model's opinion about its own length is not evidence.

      Files src/checks/limits.jstest/limits.test.js

    2. Highlighted in the asset, not summarised in a log.

      Files src/checks/claims.js

    3. A rule enforced only in the template is a suggestion.

      Files src/db/assets.js

    done when · tick each as it passes
    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.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more seo & marketing.