๐Ÿ“„ raw markdown for agents/LLMs: /API.md ยท all docs

Bluebut Read API (v1)

Read-only HTTP/JSON access to Bluebut's Facebook accounts and operational state (groups, people, posts, jobs) โ€” so people, scripts, and other Bofrid systems can fetch what the dashboard sees without the UI, the supabase service-role key, or any knowledge of keyvault/hostbun internals.

Auth

Every data endpoint needs the shared password (default ddash). Pass it either way:

# header
curl -H "Authorization: Bearer ddash" http://localhost:8787/api/v1/accounts
# or query param
curl "http://localhost:8787/api/v1/accounts?token=ddash"

/openapi.json and /docs are open (schema + a static page, no data). Override the password with API_TOKEN in the root .env. Set API_TOKEN= (empty) to disable auth entirely.

Secrets are never returned. FB passwords and cookie jars are stripped from every account row before it leaves the process โ€” callers see identity, status, and cookie presence (has_cookies, cookie_count), never the credentials.

Interactive docs

Endpoints

Method Path Returns
GET /accounts All FB accounts (keyvault registry, secrets stripped)
GET /accounts/{slug} One account
GET /accounts/health Cookie-only liveness sweep (no browser)
GET /accounts/intel Offline cookie-truth (drift / contamination / stale / dead-page)
GET /groups FB group list
GET /groups/{id} Group detail: row + members (roles) + recent posts + our memberships
GET /people People / leads (fb_person identity hub)
GET /posts Group-post log (what Bluebut has posted)
GET /jobs Live + recent job/queue state
GET /summary Counts: accounts, groups, people, posts last 7d

GET /accounts

The canonical account list = union of the bluebut-owned namespace and the droidfleet farm (ours wins). Secrets stripped.

{
  "ok": true,
  "count": 96,
  "accounts": [
    {
      "account": "pojtie",
      "display_name": "โ€ฆ",
      "account_type": "profile",
      "fb_id": "61โ€ฆ",
      "last_status": "logged_in",
      "kv_status": "active",
      "source": "chromebox_sessions",
      "proxy": { "scheme": "socks5h", "host": "โ€ฆ", "port": 1080 },
      "has_cookies": true,
      "cookie_count": 9,
      "updated_at": "2026-06-25Tโ€ฆ"
    }
  ]
}

GET /accounts/{slug}

{ "ok": true, "account": { โ€ฆ } } or 404 { "ok": false, "error": "no account \"x\"" }.

GET /accounts/health?account=<slug>

Replays each stored cookie jar against facebook.com over plain HTTPS and reads the inlined USER_ID โ†’ live/dead. No browser. Omit account for the whole fleet; pass it to check one (and get an egress-IP echo).

{ "ok": true, "total": 96, "live": 41,
  "accounts": [ { "account": "pojtie", "valid": true, "status": "logged_in",
    "fb_id": "61โ€ฆ", "expected_id": "61โ€ฆ", "name": "โ€ฆ", "http": 200,
    "via": "proxy", "egress_ip": "โ€ฆ", "ms": 740, "error": null } ] }

GET /accounts/intel?account=<slug>

Decodes + reconciles every jar offline โ€” surfaces drift / contamination / stale / dead-page that the stored fields hide. Verdicts: ok | page | drift | contaminated | stale | logged_out.

{ "ok": true, "total": 96, "counts": { "ok": 40, "page": 3, "stale": 12 },
  "accounts": [ { "account": "uthyra-se", "verdict": "contaminated",
    "detail": "jar identity is pojtie", "uid": "61โ€ฆ",
    "identity_account": "pojtie", "identity_name": "โ€ฆ",
    "stored_fb_id": "61โ€ฆ", "session_age_days": 4, "cookie_count": 9 } ] }

GET /groups

Query: limit (1โ€“500, default 100), offset, search (case-insensitive name), is_test (true|false).

curl -H "Authorization: Bearer ddash" \
  "http://localhost:8787/api/v1/groups?search=lidkoping&limit=20"

GET /groups/{id}

{id} = fb_group_id. Returns the group row plus:

Need all of a group's associated data (full roster with member identity, every post, scrape signals) in one shot, or to fetch it for every group at once? Hit the raw entity-graph surface directly โ€” see Direct entity-graph access below.

GET /people

Query: limit, offset, intent (seeker|landlord|unknown), outreach_status (new|queued|contacted|replied|converted|closed), city, first_seen_channel (admin_scrape|feed_post|dm_reply).

GET /posts

Query: limit, offset, fb_group_id, status (pending|posted|failed|skipped). Newest first.

GET /jobs

Live bridge job state: { active, queued, recent, busyAccounts }. Same shape the dashboard's Jobs page uses.

GET /summary

{ "ok": true,
  "accounts": { "total": 96, "active": 90, "logged_in": 41 },
  "groups": { "total": 412, "admin": 18 },
  "people": { "total": 5230 },
  "posts": { "last_7d": 12 },
  "generated_at": "2026-06-25Tโ€ฆ" }

Direct entity-graph access (PostgREST)

The curated /api/v1 endpoints above cover the common cases. For arbitrary reads of the whole entity graph โ€” every group with every association embedded, ad-hoc filters, joins the curated API doesn't expose โ€” query the raw PostgREST surface that the dashboard itself speaks.

Fetch every group + ALL associated data in one query

As of 2026-06-30 the entity graph carries the foreign keys PostgREST needs to embed a group's children, so a single select= pulls the whole tree:

KEY=<service-role-jwt>
curl -s -H "apikey: $KEY" -H "Authorization: Bearer $KEY" \
  "https://bluebut-rest.blpk.cc/rest/v1/fb_group?select=\
fb_group_id,name,member_count,\
fb_account_group(chromebox_account,is_member),\
fb_group_member(role,fb_user(name)),\
group_post(status,created_at),\
fb_person_signal(signal_type),\
fb_post(post_url)"

Drop the trailing &limit=1 to get all 416 groups; add &order=member_count.desc, &category=eq.housing, paginate with Range: headers, etc.

Embeddable relationships off fb_group (FK โ†’ fb_group.fb_group_id):

Embed Table Note
fb_account_group(*) fb_account_group which of our accounts are members
fb_group_member(*) fb_group_member FB roster + role; nest fb_user(*) for identity
fb_group_invite(*) fb_group_invite pending admin/group invites
group_post(*) group_post every post Bluebut published to the group
fb_post(*) fb_post feed scrape results (nest fb_person(*) for poster)
fb_person_signal(*) fb_person_signal observed events in the group

fb_group_member โ†’ fb_user, fb_post โ†’ fb_person, and fb_person_signal โ†’ fb_person are likewise embeddable, so the roster/post rows carry the person identity inline.

The FK declarations live in adapters/facebook/SCHEMA.sql (idempotent). If a select=โ€ฆ(โ€ฆ) embed ever returns PGRST200 "could not find a relationship", the FK is missing or the PostgREST schema cache is stale โ€” re-apply the schema (it ends with NOTIFY pgrst, 'reload schema').

Errors

Status Meaning
401 Missing / wrong password
404 Unknown account / group / endpoint
405 Non-GET method (API is read-only)
502 Upstream (hostbun / keyvault) failure โ€” error carries the detail

Notes