Build Sumora

KINDAreplaces $79.99/mosaves $959.88/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 single-operator outreach agent whose hard problems are treating fetched web text as hostile data rather than instruction, making every claim in a draft traceable to a span of that text, and keeping a multi-step run resumable and bounded in cost when a step fails halfway.

estimated effort weekendthe files for this build are in the project pack

RuntimeTypeScript on Node with a small local HTTP server, bound to localhostStorageSQLite, with every step written before and after it runsFetchingA headless browser for the target's own public pages onlyModelOne rented model, given only delimited untrusted text and no credentialsSendingA mail credential held by the application, behind an explicit human approval

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 agent, the local review interface and the worker.

    Get it nodejs.org, or your version manager. open ↗

    Verify node --version prints v22 or higher

  2. installfree

    Why Reading a target's own pages needs a real renderer, and this is the only heavy dependency in the build.

    Get it Install the library and run its browser download step.

    Verify A fixture page loads and readable text comes back

  3. API keypay per token; set a provider-side limit too

    Why The drafting step is the only rented part, and the ceilings in this build are what keep it from being the expensive one.

    Get it Create a key with your provider and set a spend limit in their console as well as in this application.

    Verify One run against a local fixture produces a draft with claims

  4. API keyfree tiers are ample at this volume

    Why Required only for live sending, and deliberately absent by default so that dry run is the state you cannot accidentally leave.

    Get it Any transactional sender with a verified domain you control.

    Verify A test message reaches your own inbox with the right sender

  5. decidefree

    Why The rules depend on where the recipient is, and retrofitting a contact policy after a campaign is not a thing you can do.

    Get it Write down which jurisdictions you will contact, what basis you rely on, and what source note every target must carry.

    Verify The policy is in the README and every target row has a source and a note

Data model

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

- business_context(id, body, updated_at) · your offer, tone and hard rules, edited by you and never by the model
- targets(id, company, domain, contact_name, contact_email, source, source_note, added_at, suppressed_at)
- runs(id, target_id, state, created_at, finished_at, cost_minor, failure_reason) · states are queued, running, needs_review, approved, sent, failed
- steps(id, run_id, ordinal, tool, input_json, output_json, error, tokens_in, tokens_out, cost_minor, started_at, finished_at)
- fetched_pages(id, run_id, url, fetched_at, http_status, content_sha256, text) · stored separately from anything the model is told to follow
- claims(id, draft_id, text, page_id, span_start, span_end, verified) · a claim with no span is an unsupported claim
- drafts(id, run_id, revision, subject, body, created_at, approved_at, approved_by, sent_at, message_id)
- suppression(email_or_domain, reason, added_at) · checked before a run starts and again before a send

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
MODEL_API_KEYsecretrequiredsk-...redactedThe drafting model. Give it a provider-side spend limit as well.
DATABASE_PATHrequired./data/agent.dbRuns, steps, fetched pages, claims and drafts.
RUN_COST_CEILINGrequired0.50A single run stops before crossing this figure.
DAILY_COST_CEILINGrequired5.00New runs are blocked at this figure; a run in progress finishes.
MAX_ATTEMPTSrequired3Per step. Beyond this the step fails and the run stays resumable.
SMTP_URLsecretoptionalsmtps://user:redacted@mail.example.com:465Absent by default. Sending also needs the live flag.
LIVE_SENDrequiredfalseDry run is the default and this is the second of the two locks on sending.

The build, in order

  1. A run is a ledger, not a loop

    Every step is written before it runs and completed after, so a crash resumes instead of restarting.

    1. The row is the only proof a paid call happened.

      Files src/run/ledger.tstests/resume.test.ts

    2. Restarting from the top is how you pay twice.

      Files src/run/resume.ts

    3. Two resumes on one run is two outreach emails.

      Files src/run/claim.ts

    done when · tick each as it passes
    watch out
    • Holding the run in memory and writing only the final result means a crash on step five throws away four paid model calls and a browser session, and because the retry looks identical you pay for them again. Persist before you execute, and make resume the normal way a run finishes rather than an error path you test once.
  2. Fetching is quarantine

    Page text is captured, hashed and stored where nothing can mistake it for an instruction.

    1. Concatenation is the vulnerability, not the model.

      Files src/model/prompt.tstests/injection-fixture.test.ts

    2. The evidence should be pages you could have opened yourself.

      Files src/fetch/browser.ts

    3. So the operator reviews the same text the model read.

      Files src/fetch/store.ts

    done when · tick each as it passes
    watch out
    • Concatenating fetched text straight into the prompt means any page can rewrite your agent's instructions, and the injected line does not have to be visible to a human reader to be read by the model. Keep the fetched text in its own field, wrap it in delimiters when you send it, state in the instruction that its contents are data, and show the operator the exact text the model saw.
  3. A draft that can point at its evidence

    Every factual claim in the message maps to a span of fetched text, and one that does not blocks the draft.

    1. A quote is checkable; a summary is not.

      Files src/model/draft.ts

    2. A second model call verifies nothing.

      Files src/claims/verify.tstests/verify.test.ts

    3. Reviewing is only fast when the evidence is adjacent.

      Files src/web/review.ts

    done when · tick each as it passes
    watch out
    • Asking the model to cite its sources and trusting the citation is the same mistake as trusting the summary, because a fabricated quote is as easy to generate as a fabricated fact. Verify every quoted span against the stored text by string search, and treat a quote that is not found as a failure of the draft rather than a formatting problem to repair.
  4. Ceilings, backoff and giving up loudly

    A confused run costs a known maximum and stops in a state you can see.

    1. Afterwards is one call too late, every time.

      Files src/cost/ceiling.tstests/ceiling.test.ts

    2. An uncapped retry is an uncapped bill.

      Files src/run/retry.ts

    3. A log file is where failures go to be ignored.

      Files src/web/dashboard.ts

    done when · tick each as it passes
    watch out
    • Retrying a model call that returned a malformed response, with no attempt cap, produces a loop that spends money at whatever rate the provider allows and leaves nothing to show for it. Cap attempts, check the ceiling before the call rather than after, and record a failed step rather than retrying past the cap.
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 automation.