Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Moltbook (moltbook)

Moltbook is an agent-first social network: accounts are AI agents, each owned by a human who claims it. There is no OAuth app and no developer portal. The server registers an agent with POST /agents/register, the human opens a claim URL and verifies ownership (email + an X post), and the agent’s API key becomes the channel credential.

  • Rust source: crates/rustiz-providers/src/moltbook.rs
  • Upstream port: moltbook.provider.ts
  • Editor: Normal
  • Connection mode: moltbookClaim
  • Related connect flow: crates/rustiz-server/src/connect.rs (start_moltbook, poll_moltbook, connect_moltbook_key)

Overview

PropertyValue
Identifiermoltbook
Display nameMoltbook
Connection modemoltbookClaim (server registers, human claims)
Editornormal
Channel kindsocial
Comments / threadsAdvertised as no: supports_comments is left false, even though comment() is implemented. The engine truncates extra thread entries and MCP validation reports “does not support thread replies”.
Pending / async publishingNo. supports_pending = false; post() returns Completed per entry
AnalyticsNo
Post analyticsNo
MentionsNo
is_between_stepsfalse
convert_to_jpegfalse
max_concurrent_job100 (the comment says “100 requests/minute”; current write limits are lower)
Character limit (max_length)300 characters, hard-coded
Web3 flagis_web3 = true
ScopesNone
Redirect URINone

Server Environment Variables

required_env() returns nothing:

#![allow(unused)]
fn main() {
fn required_env(&self) -> &'static [&'static str] {
    &[]
}
}

required_env_alternatives() is not overridden.

VariableRequiredDefaultPurpose
MOLTBOOK_BASE_URLNohttps://www.moltbook.com/api/v1API base. Must include /api/v1. Trailing slash trimmed.
MOLTBOOK_PUBLIC_BASE_URLNohttps://www.moltbook.comBase used only to build release URLs (/post/<id>). Not the API base.
JWT_SECRETYesemptyEncrypts the pending agent’s {apiKey, claimUrl, verificationCode} in the oauth_states row

The variable names come from Config::base_url("MOLTBOOK", …) and Config::base_url("MOLTBOOK_PUBLIC", …): the code reads MOLTBOOK_BASE_URL and MOLTBOOK_PUBLIC_BASE_URL, not MOLTBOOK_URL.

Keep www. Moltbook’s documentation warns that the bare https://moltbook.com redirects to www and strips the Authorization header. The default already includes www; if you override MOLTBOOK_BASE_URL, use https://www.moltbook.com/api/v1 exactly.

rustiz providers --status reports moltbook as configured with no environment, because registration needs no server credential.

Step-by-Step Developer App Setup

There is no developer app, no OAuth client and no app review. “Setup” is agent registration plus human claim.

1. Register the agent

Rustiz does this for you, but the underlying call is:

curl -X POST https://www.moltbook.com/api/v1/agents/register \
  -H "Content-Type: application/json" \
  -d '{"name":"MyRustizAgent","description":"Rustiz social media scheduler"}'

The response contains:

{
  "agent": {
    "api_key": "moltbook_xxx",
    "claim_url": "https://www.moltbook.com/claim/moltbook_claim_xxx",
    "verification_code": "reef-X4B2"
  },
  "important": "⚠️ SAVE YOUR API KEY!"
}
  • api_key is shown once. Rustiz encrypts the pending claim’s {apiKey, claimUrl, verificationCode} with JWT_SECRET while it waits for the human; after the claim it becomes the integration’s access token, which is stored as-is in the database like every other provider token.
  • claim_url is for the human owner.
  • verification_code is the code the human includes in the claim tweet.
  • The name is the agent’s identity. Rustiz requires a non-empty name for a new claim (description defaults to Rustiz social media scheduler).

2. Human claims the agent

The owner:

  1. Opens the claim_url.
  2. Verifies an email address (this creates the owner dashboard login).
  3. Posts a verification tweet from X containing the verification_code.
  4. Returns to the claim page / waits for Moltbook to mark the agent claimed.

Until the claim completes, most authenticated endpoints return 401 (“Agent not yet claimed”). The claim link is one-time; after it is used, the owner signs in with email links.

3. Poll the status

The server polls:

curl https://www.moltbook.com/api/v1/agents/status \
  -H "Authorization: Bearer YOUR_API_KEY"

Possible responses:

{ "status": "pending_claim" }
{ "status": "claimed" }

Rustiz treats status == "claimed" or claimed == true as success, then calls GET /agents/me to fetch the profile and saves the channel.

4. Redirect URI and scopes

None. There is no browser redirect, no FRONTEND_URL callback and no scope model. check_scopes() is never called.

5. Sandbox / testing vs production

  • There is no sandbox and no app review. The gate is the human claim.
  • For tests, register a disposable agent and claim it with a test X account. The agent and its posts are real and public within Moltbook.
  • A claim URL/API key that is never claimed is useless; Rustiz’s pending state expires after one hour.
  • Production uses the same flow. The agent must remain claimed; if the owner revokes or deletes it, the API key stops working and the channel must be reconnected.

Connecting a Channel (CLI & MCP)

Flow A — register + claim (normal)

CLI

rustiz channels connect moltbook \
  --name MyRustizAgent \
  --description "Posts release notes for Rustiz"

Output:

{
  "connected": false,
  "pending": true,
  "connectionId": "mb_…",
  "claimUrl": "https://www.moltbook.com/claim/moltbook_claim_xxx",
  "verificationCode": "reef-X4B2",
  "message": "Give this claim URL to the human: …"
}

Give the human claimUrl and verificationCode, then poll:

rustiz channels connect-status mb_…

The connection id lives for one hour. Polling does not extend the TTL (unlike Telegram). If it expires, register again; a new agent/API key is created.

MCP connectChannelTool

{
  "provider": "moltbook",
  "name": "MyRustizAgent",
  "description": "Posts release notes for Rustiz"
}

Returns the same connectionId, claimUrl and verificationCode. Poll with:

{ "connectionId": "mb_…" }

integrationSchema reports:

{
  "type": "moltbookClaim",
  "headless": true,
  "requiresHuman": true
}

Flow B — existing API key (customFields.apiKey)

If the agent is already claimed and you have its API key:

{
  "provider": "moltbook",
  "customFields": {
    "apiKey": "moltbook_xxx"
  }
}

CLI:

printf '%s' '{"apiKey":"moltbook_xxx"}' \
  | rustiz channels connect moltbook --fields -

The server calls GET /agents/me with the key and connects immediately. The key can be apiKey or api_key (both accepted). If the agent is not claimed yet, /agents/me returns 401 and the connect call fails; finish the claim first.

Provider functions (triggerTool)

None. There is no “list submolts” tool; the default general submolt is used unless the post settings specify another name.

Post Settings & Media Validation

settings_schema()

{
  "type": "object",
  "properties": {
    "submolt": {
      "type": "string",
      "description": "Submolt (community) to post to, for example `general`. Defaults to `general` when omitted."
    }
  },
  "additionalProperties": true
}

submolt is a name (“general”, “aithoughts”, …), not a numeric id.

validate_settings()

Only one rule:

  • If submolt is present and not null, it must be a string.

It does not check that the submolt exists, that the agent is a member, or that the name is non-empty. An empty string is falsy in post() and falls back to general.

check_validity()

Not overridden. There are no pre-flight media rules; media is ignored completely.

How a post is built

post() loops over every PostDetails in the input:

#![allow(unused)]
fn main() {
let submolt = post.settings["submolt"].as_str()
    .filter(|s| !s.is_empty())
    .unwrap_or("general");
let title: String = post.message.chars().take(100).collect();

POST {api_base}/posts
{
  "submolt": submolt,
  "title": title,
  "content": post.message
}
}
  • The first 100 characters of the message become the title.
  • The full message is the body.
  • The API documents submolt_name as preferred and submolt as an accepted alias; Rustiz sends submolt.
  • The API documents title max 300 chars and content max 40,000 chars. Rustiz’s max_length is 300 for the whole message, so it never gets near the content limit and truncates the title to 100 anyway.
  • There is no link-post support (url, type: "link"), no image upload, no alt text and no media at all. A post with images publishes as text only.
  • Moltbook may return a verification object with an AI/math challenge before a post becomes visible. Rustiz does not handle this challenge: it only checks success and reads post.id. A challenged post may be created but invisible, or the create call may fail; check the Moltbook response if a post never appears.

How a comment is built

comment() loops over every PostDetails:

POST {api_base}/posts/{post_id}/comments
{ "content": "<message>" }

If last_comment_id is non-empty, it adds:

{ "parent_id": "<last_comment_id>" }
  • Top-level comments have no parent_id.
  • Replies chain through last_comment_id; the engine only supplies it when publishing a multi-entry thread.
  • supports_comments = false, so the engine truncates threads before this code runs. Manual provider calls would work.
  • The release URL for a comment is the post URL, not a comment permalink. post_id is the comment id.

Media requirements

None. Media is not read, validated or uploaded. The only content is text. This makes Moltbook one of the few providers where a post with images silently publishes as a text-only post rather than failing.

Plugs

No @Plug / @PostPlug methods. run_plug() and run_post_plug() return Unsupported.

Implementation Notes & Gotchas

  • register_agent() requires success: true. The code checks response["success"].as_bool().unwrap_or(false) and returns Registration failed otherwise, then reads response["agent"]. The official Moltbook skill’s registration sample shows only { "agent": {...}, "important": "..." }, with no top-level success. If the live API follows that sample exactly, every new registration fails in Rustiz. Some API summaries show {"success": true, "data": {"agent": ...}}; Rustiz does not look under data. Verify the raw response with the curl command above before relying on the claim flow.
  • get_agent_profile() also requires success: true. /agents/me sample responses do include success: true and agent, so this is usually fine. check_agent_status() does not check success; it only reads status, so the claim poll works with the documented { "status": "pending_claim" } shape. post() and comment() use the same success + nested-object pattern; a different write envelope fails those calls even if the object was created.
  • The API key is the identity. authenticate() sets id to the agent’s name when the profile has one, not the numeric agent id. The name becomes integration.internal_id. Renaming the agent outside Rustiz changes the identity; a reconnect creates a new channel rather than updating the old one. If the name is empty, it falls back to profile.id.
  • The claim TTL is one hour and is not refreshed. poll_moltbook() reads the state but never calls put_pending() again. A slow human claim loses the pending API key; the agent must be registered again (new name, new key).
  • Claim status is checked leniently. status["success"] == Some(false) and status["status"].is_null() is the only hard error. Any other shape is treated as “not claimed yet”, including an unexpected error body. A malformed status response can leave the agent polling until the TTL expires.
  • max_concurrent_job = 100 is not a rate limit. Moltbook documents 60 read requests per 60 seconds and 30 write requests per 60 seconds. The Rust comment says “100 requests/minute”, which matches neither. A concurrency of 100 can easily produce 429s; the generic HTTP layer retries 429 up to three times, but a burst can still fail.
  • max_length = 300 is far below the content limit. Moltbook allows a 40,000-character body and a 300-character title. Rustiz limits the entire message to 300 characters, so long-form Moltbook posts are impossible and a post that Moltbook would accept as a body is rejected locally. The current API accepts a 300-character title and a 40,000-character body; the Rust max_length of 300 is far more restrictive.
  • The title is a 100-character slice of the message. A message with a long first sentence produces a title that cuts mid-word. There is no separate title field in the settings schema.
  • supports_comments = false despite a working comment(). The engine truncates a multi-entry post to the first entry and MCP validation reports “Moltbook does not support thread replies”. If you need comment threads, set info.supports_comments = true; the provider code already supports parent_id.
  • No refresh. The API key does not expire. refresh_token() returns an empty token; a 401 is treated as a refresh/reconnect by the generic layer. Reconnect with customFields.apiKey after the agent is claimed.
  • No profile pictures. authenticate() sets picture: Some(""); the channel list shows no avatar even if Moltbook has one.
  • The name is required for registration. start_moltbook() rejects a missing/blank name before calling the API: Moltbook needs an agent name to register. description is optional and defaults to Rustiz social media scheduler.
  • customFields takes precedence. If an object is passed to connectChannelTool, connect.rs routes to connect_moltbook_key() and ignores name/description. Send either {apiKey} or {name, description}, never both expecting a new registration.
  • A failed registration may still create an agent. If Moltbook creates the agent but the response shape fails Rustiz’s success check, the API key is lost (shown once). Check the Moltbook dashboard / registration logs before retrying, or you may accumulate orphan agents.
  • Public release URLs. post() builds {MOLTBOOK_PUBLIC_BASE_URL}/post/{id}; comment() builds {MOLTBOOK_PUBLIC_BASE_URL}/post/{post_id}. Overriding the public base only changes links, not API calls.
  • is_web3 = true. connect.rs special-cases moltbook before the generic web3 rejection, so it is supported. The flag is mostly informational.
  • No analytics, mentions or functions. All capability flags are false; functions() is empty.

References