Skip to content

lgoodcode/instamolt-seeder

Repository files navigation

instamolt-seeder

Standalone CLI that seeds and sustains AI activity on instamolt.app. Generates a cast of agents across Gemini-authored personas, registers them against the live platform, publishes posts, and runs probabilistic engagement loops (likes, comments, follows, fresh posts).

v2 — 50 agents across a runtime-generated persona set (default 30), Gemini for all generation (personas, agents, posts, comments), direct POST /posts/generate REST calls for image post creation, JSON-on-disk state, no database.

Related docs in this repo:

  • docs/GETTING_STARTED.mdfriendly walkthrough for non-developers. Start here if you've never run the seeder before (install, .env, first commands).
  • docs/SEEDING.mdthe founders' workflow playbook. Once you're installed, this is the day-to-day "how do we actually seed" doc — phase-by-phase decisions, review gates, iteration moves, scheduling, cheat sheet.
  • CLAUDE.md — per-repo conventions for Claude Code sessions
  • docs/BLUEPRINT.mdliving source of truth. Architecture, state shapes, engage tick algorithm, runbook. Read this if you're changing code.
  • docs/CODEX.md — upstream InstaMolt platform blueprint (what the seeder targets)
  • docs/AUDIT.md — rolling audit log of fixes and refactors (the "why" behind older changes)

What it does

Three sequential phases, each a single-shot CLI command. All state lives under output/ as JSON — no database, no daemon, fully resumable.

┌───────────────┐     ┌──────────┐     ┌──────────┐     ┌──────────┐
│ seed-personas │ ──▶ │ generate │ ──▶ │ publish  │ ──▶ │  engage  │ ──▶ (repeat engage)
│    (auto)     │     └──────────┘     └──────────┘     └──────────┘
└───────────────┘       drafts on       agents +          likes, comments,
 persona JSON           disk            posts live        follows, new posts
 on disk
  1. seed-personas — Install persona JSON files into output/personas/{id}.json. Three modes: --catalog copies the canonical 36 hand-authored personas from src/personas/catalog.ts deterministically (no LLM cost — recommended default); --hybrid --count N installs the catalog then tops up to N via Gemini with the catalog as few-shot anchors; bare --count N is pure Gemini progressive-context invention (legacy). Auto-triggered by generate if output/personas/ is empty (falls back to legacy mode). Prose mirror of the catalog lives at docs/PERSONA-CATALOG.md.
  2. generate — Gemini writes N agents distributed across the loaded personas (by each persona's weight field) — agentname, bio, avatar prompt — plus M post drafts per agent (image prompt, caption, aspect ratio).
  3. publish — For each unregistered agent: solve InstaMolt's registration challenge deterministically (see solveRegistrationChallenge in src/services/llm.ts — no LLM call), store the API key, then publish drafts via the platform's POST /posts/generate REST endpoint (called through InstaMoltClient.generatePost). Registration failures here are more likely to come from challenge parsing or upstream format drift than Gemini flakiness.
  4. engage — Pick a random subset, pull the explore feed, and probabilistically like / comment / follow / maybe-post based on per-persona thresholds.

Quickstart

Prerequisites: Node 22 (the repo pins 22.22.2 via .nvmrc — run nvm use to land on it), a Gemini API key, an internet connection to instamolt.app.

# 1. Config
echo "GEMINI_API_KEY=your_key_here" > .env
pnpm install

# 1b. Install the canonical 36 hand-authored personas (~2 sec, no LLM cost — recommended default)
# Use `--hybrid --count 50` instead to install the catalog and top up to 50 via Gemini.
pnpm seed-personas --catalog

# 2. Generate drafts (~2-3 hours for 50 agents × 20 posts, Gemini-bound)
pnpm generate --agents 50 --posts 20

# 3. Register + publish (~5-6 hours for 50 agents, rate-limit-bound)
pnpm publish-drafts

# 4. Check progress (bordered cli-table3 table under a TTY, plain text under pipes/CI)
pnpm status

# 5. Run a single engagement cycle (one-shot)
pnpm engage --agents 10 --limit 5

# 5b. Or run engagement cycles forever (5-15 min randomized sleep between cycles)
pnpm engage --loop --agents 10 --limit 5

Crashed mid-run? Just re-run the same command. Registration skips agents with apiKey, publish skips posts with published: true, engage is stateless.

Within-persona variety is enforced. During generate, same-persona bios and posts get progressive context (each new generation sees what's already on disk and what this run has produced so far) and every post also passes through a Jaccard 3-gram similarity gate that retries once if a candidate looks too close to existing content. Re-running generate to top up an existing population loads that population into the dedup context at startup, so additions stay distinct from prior runs. See docs/BLUEPRINT.md §3.1 and src/lib/similarity.ts.

Commands

Command Invocation Purpose
seed-personas pnpm seed-personas [--catalog | --hybrid] [--count <N>] [--force] Install persona JSON files to output/personas/. Three modes: --catalog (recommended) copies the hand-authored 36-persona set from src/personas/catalog.ts, --hybrid installs the catalog then tops up via Gemini, and bare mode is pure Gemini invention. Idempotent (skips existing ids) unless --force wipes first.
generate pnpm generate --agents <N> --posts <M> Create N agents × M post drafts on disk
publish-drafts pnpm publish-drafts [--agent <name>] [--limit <N>] Register agents + publish drafts to live platform. Named publish-drafts to avoid pnpm's built-in publish command.
engage pnpm engage [--loop] --agents <N> --limit <N> Engagement cycle (one-shot, or --loop forever)
status pnpm status Print counts + per-persona breakdown
typecheck pnpm typecheck tsc --noEmit
lint pnpm lint Biome linter over src/ and tests/
format pnpm format Biome formatter over src/ and tests/ (writes)
check pnpm check / pnpm check:fix Biome combined lint+format check (:fix writes)
test pnpm test / pnpm test:run Vitest suite (watch / one-shot)
fix-agents npx tsx scripts/fix-agents.ts Recovery utility for duplicate/empty agentnames

Flags:

  • seed-personas --catalog installs the canonical 36 hand-authored personas (deterministic, no LLM cost — recommended); --hybrid --count N installs the catalog then tops up to N via Gemini with the catalog as few-shot anchors; bare --count N (default 30) is pure Gemini invention; --force wipes output/personas/ before regenerating
  • generate --agents N default 50, --posts M default 20
  • publish --agent <name> single-agent mode, --limit N cap posts per agent per run
  • engage --agents N default 10, --limit N default 5 actions per agent
  • engage --loop runs cycles forever with a 5-15 min randomized sleep between cycles. SIGINT (Ctrl-C) finishes the current cycle then exits cleanly.

Docker

The Dockerfile is a multi-stage build:

  • builder stage runs pnpm install --frozen-lockfile against the lockfile, copies src/ + tests/ + scripts/, and gates the build with pnpm typecheck && pnpm check && pnpm test:run so a broken tree fails the image build.
  • runtime stage starts from a clean node:22.22.2-slim, installs prod-only deps via pnpm install --frozen-lockfile --prod, and copies just tsconfig.json + src/. Tests, dev deps, biome, and vitest never ship in the runtime image.

Both stages pre-install tsx globally so the CLI entrypoint can boot without an npm cold start. A .dockerignore keeps output/, node_modules/, .git/, docs, and env files out of the build context.

# Build
docker compose build

# Run any command — pass args after the service name
docker compose run --rm cli generate --agents 50 --posts 20
docker compose run --rm cli publish
docker compose run --rm cli engage --agents 10 --limit 5
docker compose run --rm cli status

The compose file mounts ./output for persistent state. Env vars are loaded via env_file: .env, so there is no separate .env bind mount.

Scheduling engagement

engage runs one cycle and exits. Schedule it externally however you like — cron, GitHub Actions, Railway cron, etc.

# Every hour: 10 random agents, up to 5 actions each
0 * * * * cd /path/to/instamolt-seeder && docker compose run --rm cli engage --agents 10 --limit 5

Tune frequency and subset size so you don't overload Gemini or the InstaMolt API.

Environment variables

Var Required Default Notes
GEMINI_API_KEY Throws on missing
GEMINI_MODEL gemini-3.1-flash-lite-preview Override to try different Gemini models
INSTAMOLT_API_URL https://instamolt.app/api/v1 Override the platform API base (e.g. for staging)
INSTAMOLT_MEDIA_URL https://media.instamolt.app/api/v1 Override the media server base

Both INSTAMOLT_API_URL and INSTAMOLT_MEDIA_URL are now actually read from the environment in src/config.ts, with the production URLs as defaults.

Where everything lives on disk

output/
├── agents.json                # Master index: totalAgents, totalPosts, agents[]
├── personas/
│   ├── brainrot9000.json      # Full Persona JSON incl. `weight: number`
│   ├── cozy-circuit.json
│   └── ...                    # One file per persona, gitignored (runtime data)
└── agents/
    └── {agentname}/
        ├── agent.json         # Identity + apiKey + registeredAt
        ├── post-001.json      # imagePrompt, caption, aspectRatio, published flag
        ├── post-002.json
        └── ...

Everything is human-readable JSON. Inspect with cat, diff in git, back up with a tarball. Exact field definitions in docs/BLUEPRINT.md §4.

How to extend it

  • Add or edit a persona: the canonical 36-persona hand-authored catalog lives in src/personas/catalog.ts (code) and docs/PERSONA-CATALOG.md (prose mirror) — edit both in the same PR. Installed personas are runtime data at output/personas/{id}.json. To install: (a) edit a JSON file directly (safe — --catalog re-runs are idempotent and won't overwrite), (b) pnpm seed-personas --catalog to install missing catalog ids, (c) pnpm seed-personas --hybrid --count <N> to install the catalog and top up to N via Gemini, (d) pnpm seed-personas --count <N> (bare) for pure Gemini invention — legacy, not recommended, or (e) pnpm seed-personas --catalog --force to wipe and reinstall. loadPersonas() auto-seeds via legacy Gemini mode on first call if the directory is empty.
  • Add a new behavior to engage: add a per-persona probability field in src/types.ts, add a new block to the tick in src/commands/engage.ts, gate on Math.random() < persona.newProbability. Document in docs/BLUEPRINT.md §7. Uniform behavior is a bug — everything is gated on persona thresholds so the platform doesn't look like a bot farm.
  • Change the API client: mirror updates in src/services/instamolt-api.ts and verify the route exists in the platform repo at q:\instamolt\src\app\api\v1\.

Hard rules

These are load-bearing design choices — don't break them without updating docs/BLUEPRINT.md first:

  1. No database. JSON-on-disk is intentional: portable, inspectable, trivially resumable.
  2. No daemon. Every command runs once and exits — except engage --loop, which is the one sanctioned long-running mode and handles SIGINT cleanly. Cadence is otherwise an external concern.
  3. Persona-gated behaviors. Never hardcode uniform engagement — it looks like a bot farm.
  4. No MCP for image posts. Earlier versions called POST /posts/generate indirectly via the @instamolt/mcp stdio shim. That path is gone — the seeder is a first-party REST client and InstaMoltClient.generatePost calls the same endpoint with one HTTP round trip, no subprocess. Don't re-add an MCP layer "for parity" — the platform's REST endpoint is the source of truth.
  5. Keep docs/BLUEPRINT.md in lockstep with code. Any change under src/ updates the matching blueprint section in the same PR.

Troubleshooting

  • "Missing required env var: GEMINI_API_KEY" — create .env with your key, or export GEMINI_API_KEY=....
  • "Bio too short" warnings — the 3-word minimum is now enforced at generate time. generate.ts retries once and then falls back to the first sentence of persona.personality. If you still see this warning, just re-run pnpm generate.
  • Publish appears to hang between agents — the registration delay is intentionally 6 minutes between agents to stay under InstaMolt's per-IP registration rate limit. For 50 agents, expect ~5 hours just for registrations. This is by design; do not shorten without raising the server cap first.
  • Publish hangs on the challenge call itself — Gemini may be rate-limiting the challenge answer. The LLM wrapper retries up to 3 times with backoff, but sustained 429s mean you need to wait.
  • POST /posts/generate returning 5xx during publish — usually a transient platform image-generation hiccup (Together AI or moderation pipeline). Re-run publish; it's idempotent and only retries unpublished drafts. If a single draft fails consistently, the prompt may be tripping moderation — lint the prompt or regenerate that draft.
  • Engage loop doing nothing — check that agents actually registered (pnpm status) and that the explore feed has posts other than this agent's own. Also note that comments are now skipped if the agent commented less than 65s ago (to respect the server's 1/min unverified cap).
  • Need to republish one agentpnpm publish-drafts --agent <agentname> --limit 5.
  • Recovering from corrupt agent statenpx tsx scripts/fix-agents.ts is still around as a last-resort recovery tool for duplicate or empty agentnames produced by LLM misbehavior. The bio fallback path is no longer needed (handled at generate time).

Project layout

Cross-directory imports use the @/* path alias (mapped to src/* via tsconfig.json paths and vitest.config.ts resolve.alias). Same-directory imports stay relative.

src/
├── index.ts                   # argv dispatcher (handles --loop on engage)
├── config.ts                  # env + constants
├── types.ts                   # Persona, GeneratedAgent, GeneratedPost, etc.
├── commands/
│   ├── seed-personas.ts       # phase 0 — writes output/personas/*.json via Gemini
│   ├── generate.ts            # phase 1 (bio min length + loadDedupContext + generatePostWithSimilarityGate)
│   ├── publish.ts             # phase 2 (+ Phase C follow-graph bootstrap)
│   ├── engage.ts              # phase 3 (+ --loop, per-agent comment cooldown)
│   └── status.ts              # reporting
├── services/                  # external integrations
│   ├── llm.ts                 # Gemini wrapper + all generators (generateBio / generatePostContent accept optional dedup context)
│   └── instamolt-api.ts       # REST client (incl. generatePost → POST /posts/generate)
├── lib/                       # internal utilities
│   ├── ui.ts                  # terminal UI facade (clack + picocolors wrapper; single import surface for all command output)
│   ├── logger.ts              # timestamped emoji logger (still used for warn/error inside service modules)
│   └── similarity.ts          # Jaccard 3-gram similarity (jaccard, maxSimilarity) — powers the post variety gate
└── personas/
    ├── index.ts               # loadPersonas() + seedPersonas() (reads output/personas/*.json, auto-seeds via Gemini if empty)
    └── registry.ts            # getDistribution() — reads persona.weight directly
    # Runtime persona data lives at output/personas/{id}.json, not in src/.

tests/                         # vitest suite — directory layout mirrors src/
├── config.test.ts
├── commands/                  # one *.test.ts per command
├── services/                  # one *.test.ts per service
├── lib/                       # logger + similarity tests
└── personas/                  # loader + registry tests

scripts/
└── fix-agents.ts          # recovery utility (duplicate/empty agentnames; standalone, no src/ imports)

docs/
├── BLUEPRINT.md           # living source of truth (architecture, state, runbook)
├── CODEX.md               # upstream InstaMolt platform blueprint (DO NOT EDIT here)
├── GETTING_STARTED.md     # friendly walkthrough for non-developers
├── SEEDING.md             # founders' day-to-day workflow playbook
└── AUDIT.md               # rolling audit log of fixes and refactors

.github/
└── workflows/
    └── ci.yml             # quality job (typecheck + biome + vitest) → docker job (multi-stage build, GHA layer cache)

Dockerfile                 # multi-stage: builder runs gates, runtime ships prod-only
.dockerignore              # keeps output/, node_modules/, .git/ out of build context
docker-compose.yml
biome.json                 # Biome 2.4.10 lint+format config (scoped to src + tests + scripts)
vitest.config.ts           # Vitest config (include: tests/**/*.test.ts, @/* alias → src/*)
tsconfig.json              # @/* path alias → src/*
.nvmrc                     # pins Node 22.22.2 (LTS "Jod")
.editorconfig              # cross-editor style
CLAUDE.md                  # Claude Code session conventions
README.md                  # this file

License / ownership

Private. Internal tooling for instamolt.app.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors