Run an Instagram brand from any agent · one API, one CLI, one skill.
What's inside: the @rizzailab/onbrand CLI, two skills · onbrand and humanizer, the Claude Code plugin + marketplace, and the publish guide.
humanizer is the anti-slop standard OnBrand enforces on every generated line: banned words and phrases, no long dashes, no "not just X it's Y", no preamble, no burned-in text on images. Install it on its own with npx skills add rizzailab/humanizer.
Reads SKILL.md straight from the public repo. No registry.
npx skills add rizzailab/onbrand
The repo is its own marketplace. Two commands inside Claude Code.
/plugin marketplace add rizzailab/onbrand /plugin install onbrand@rizzailab
Plain Node ESM, no dependencies. The key is stored at ~/.onbrand/config.json.
npm i -g @rizzailab/onbrand onbrand login --key ob_live_...
Agents can also skip the CLI entirely and call the HTTP API directly.
https://onbrand.rizzgroup.org/api/v1
Mint a key on the API keys screen in the app. It looks like ob_live_... and is shown once. Send it as Authorization: Bearer ob_live_...
read · generate · ingest · schedule · approve · publish
New keys default to read + generate. Approve and publish are opt-in. A key can be locked to a single brand, and it then refuses every other brand with 403.
| TOOL | METHOD | PATH | SCOPE | PURPOSE |
|---|---|---|---|---|
| brands | GET | /api/v1/brands | read | List the brands this key can act on. A brand-scoped key returns exactly one. |
| generate | POST | /api/v1/generate | generate | Produce a post with OnBrand's own engines. Use dry_run first to validate a brief for free. |
| ingest | POST | /api/v1/ingest | ingest | Push media made outside OnBrand into a brand. Stored as a real asset labelled external. |
| posts | GET | /api/v1/posts | read | List a brand's posts, newest first. |
| approve_post | POST | /api/v1/posts/:id/approve | approve | Approve a post. Same guards, verdict and teaching loop as the dashboard. |
| reject_post | POST | /api/v1/posts/:id/reject | approve | Reject or send back a post. Writes the verdict and triages the failure. |
| schedule | POST | /api/v1/schedule | schedule | Move or clear a post's slot through the same guarded path the calendar uses. |
| publish | POST | /api/v1/publish | publish | Publish a post now. The channel decides real or simulated, and simulated is always labelled. |
| analytics | GET | /api/v1/analytics | read | Real Instagram numbers for a brand plus that brand's AI spend. Never estimated. |
| insights | GET | /api/v1/insights | read | The brain's latest written insights for a brand. |
| credits | GET | /api/v1/credits | read | Plan, credit balance and period end for the account behind this key. |
| manifest | GET | /api/v1/manifest | none | This document. Public, no key required. |
success { "ok": true, "data": { ... } }
failure { "ok": false, "error": "human readable", "code": "machine_readable" }curl -s https://onbrand.rizzgroup.org/api/v1/brands \ -H "Authorization: Bearer ob_live_xxx"
curl -s -X POST https://onbrand.rizzgroup.org/api/v1/generate \
-H "Authorization: Bearer ob_live_xxx" \
-H "Content-Type: application/json" \
-d '{"brand_id":"<uuid>","brief":"rooftop golden hour, film grain","type":"image","dry_run":true}'A dry run resolves the prompt, names the engine and model, and returns estimated_credits with spent:false. No engine call, no asset, no post row.
Key lookup order: --key → ONBRAND_API_KEY → ~/.onbrand/config.json. Base url: --api-url → ONBRAND_API_URL → https://onbrand.rizzgroup.org. Add --json to any command for the raw envelope. Every failure exits non-zero.
| COMMAND | SCOPE | WHAT IT DOES |
|---|---|---|
| onbrand manifest | none | Print the tool catalogue straight from the API. |
| onbrand login --key <ob_live_...> | read | Save the key to ~/.onbrand/config.json (mode 600) and verify it. |
| onbrand brands | read | Brands the key can act on, with autopilot state and approval threshold. |
| onbrand credits | read | Plan, credit balance, period end. |
| onbrand generate --brand <id> --brief "<text>" [--kind image|reel|carousel] | generate | Create a candidate post. Spends credits. The API also accepts dry_run for a no-spend preview. |
| onbrand posts [--brand <id>] [--status <s>] [--limit N] | read | List posts, newest first. |
| onbrand approve <post_id> | approve | Approve, schedule into the next free slot, teach the Taste Engine. |
| onbrand reject <post_id> [--reason "<text>"] | approve | Reject and record why. |
| onbrand schedule <post_id> --at <ISO8601> | schedule | Move the post to a slot. |
| onbrand publish <post_id> | publish | Publish now. The response says mode: real, simulated or reminder. |
| onbrand insights --brand <id> | read | The brain's latest written insights. |
| onbrand analytics --brand <id> | read | Real Instagram numbers plus AI spend. |
| onbrand ingest --brand <id> --url <media url> [--source <tool>] | ingest | Pull in media made elsewhere. |
Publish returns a top-level mode: real, simulated or reminder, plus a simulated boolean. Placeholder numbers only ever appear under simulated_preview. A simulated result is never dressed as real, and a business refusal comes back as 409 with a machine code, never a 500.
onbrand publish <post_id> --json
# => { ok: true, data: { mode: "simulated", simulated: true, ... } }Anything produced outside OnBrand · Midjourney, a local model, a photographer · can enter a brand and then be scored, approved, scheduled and published like anything else. It is stored as a real asset labelled external, and external media never teaches the Taste Engine.
Mint a key carrying the ingest scope, then POST the media. Send a fetchable media_url, or POST raw bytes as application/octet-stream with the same fields as query params. Optional fields: kind (image or video), prompt, version, and request_id to fulfil a specific connector request.
curl -s -X POST https://onbrand.rizzgroup.org/api/v1/ingest \
-H "Authorization: Bearer ob_live_xxx" \
-H "Content-Type: application/json" \
-d '{"brand_id":"<uuid>","source":"midjourney","media_url":"https://cdn.example/img.png","kind":"image","prompt":"rooftop golden hour --ar 4:5"}'In the app, register an external generator on the brand. A connector carries a provider, a label and a mode: manual, api (it gets its own scoped key) or runner (a machine you host polls for requests). Each brand also has a drop token, which gives you two dumb-simple inlets:
Manage both in the app: External generators and Drops.
One folder ships three artifacts from one push to a public repo. Two of them need no registry at all.
Public repo, no registry submission. The resolver reads skills/onbrand/SKILL.md straight from the repo.
npx skills add rizzailab/onbrand
No registry either. marketplace.json makes the repo its own marketplace, plugin.json owns the version. Validate before pushing.
claude plugin validate ./ --strict /plugin marketplace add rizzailab/onbrand
Scoped packages are private by default, so public access is not optional. A GitHub Release also triggers the publish workflow.
npm login npm version patch npm publish --access public