Can Hermes Agent post to Instagram, LinkedIn, X and TikTok?

Yes, after you plug in a publisher. Hermes will not grow a Graph API out of the learning loop. Nous Research’s docs do not ship Instagram or LinkedIn publishers. You add a hosted MCP server under mcp_servers in ~/.hermes/config.yaml, put a scoped key in ~/.hermes/.env, install the skill so the model does not invent UTC, reload, call list_accounts, then save drafts. Cron may wake and write. Cron may not call publish_now.

I build Social by InstantDM. We sell the scheduler. Same MCP URL we give Claude and OpenClaw: https://social-api.instantdm.com/mcp. Swap the URL if you use someone else; the YAML shape stays. This page is the Hermes-shaped wiring — every command, every payload, every failure I have watched an always-on learner walk into.

Hermes Agent homepage — Nous Research's self-hosted runtime hermes-agent.nousresearch.com. Notice it is a runtime with memory, cron and a learning loop. There is no social calendar and no OAuth button. Publishing is a separate API.

If you are mixing the Hermes LLM with Hermes Agent, stop. The models are fine-tunes. The agent is the Python process that calls tools. You can point the agent at Claude, GPT, a Hermes checkpoint, OpenRouter, or Nous Portal. Definition: What is Hermes Agent?. Comparison: OpenClaw vs Hermes. Protocol: MCP for social media.

What do you need before you edit any YAML?

  • Hermes installed and hermes doctor clean. Linux, macOS, WSL2: curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash, then source ~/.bashrc and hermes setup. Windows PowerShell: iex (irm https://hermes-agent.nousresearch.com/install.ps1). Do not grant posting tools to a doctor that is already red.
  • A scheduler workspace with connected social accounts. social-app.instantdm.com. Official OAuth only. Instagram needs a professional account. One workspace per brand.
  • A scoped API key with accounts:read, posts:read, posts:write, media:write. Optional analytics:read. Leave platform:proxy off.
  • Gateway running if you talk from Telegram (hermes gateway). CLI-only users still need the env the process actually sees.
  • A SOUL.md that forbids publish_now unless you said “publish now.” Hermes’s learning loop will try to skill-ize a successful live post. SOUL is the constitution. Skills are statutes.

Hermes lives in YAML and cron. That is the appeal. It is also why a bad prompt in hermes cron create repeats every weekday at 08:00 until you notice.

How do you put the API key where the gateway can see it?

~/.hermes/.env:

SOCIAL_BY_IDM_API_KEY=sk_live_YOUR_KEY

No quotes unless the value itself contains spaces (it should not). No export keyword in .env. No trailing space. The file should not be in git. Mode:

chmod 600 ~/.hermes/.env

The gateway process must be restarted — or at least re-read env — after you add the line. A key in .env that the CLI sees and the gateway does not is the number-one “it asked me to paste the key in Telegram” bug. Telegram is not a secret store. If the key already landed in a chat, rotate it in the dashboard (Developer → API keys) and update .env.

Do not put the raw key in config.yaml if you can interpolate:

headers:
  X-Api-Key: "${SOCIAL_BY_IDM_API_KEY}"

Some Hermes builds want Authorization: Bearer ${SOCIAL_BY_IDM_API_KEY} instead of X-Api-Key. Our server accepts both on the REST side and the MCP side. If tools/list is empty, try the other header before you rotate the key. Rotating a working key because the client sent the wrong header wastes a morning.

URL-only fallback, if a given Hermes build cannot set headers:

https://social-api.instantdm.com/mcp?key=sk_live_YOUR_KEY

Treat that URL as a secret. Prefer headers.

How do you add the MCP server in config.yaml?

~/.hermes/config.yaml:

mcp_servers:
  social-by-idm:
    url: "https://social-api.instantdm.com/mcp"
    headers:
      X-Api-Key: "${SOCIAL_BY_IDM_API_KEY}"
    enabled: true

That is the whole block. Indentation matters. Tabs vs spaces matter. A duplicate mcp_servers: key later in the file silently wins or silently loses depending on the YAML library — grep the file once.

Then:

/reload-mcp

in the CLI session, or restart the gateway:

hermes gateway

Some versions ship hermes mcp install or hermes mcp add. If yours does, it writes the same YAML. Read the file after it runs. Do not assume the wizard picked Streamable HTTP; our server is remote Streamable HTTP, not a local stdio process. There is nothing to pip install for the publisher.

Ask:

What social tools do you have?

You want list_accounts, create_post, update_post, get_post, get_post_status, list_posts, delete_post, upload_media_from_url, create_upload_url, complete_upload, list_media, delete_media, get_analytics, list_platform_posts, delete_platform_post, platform_request16 tools, full REST parity. Product: /mcp.

Hermes Agent documentation site — install, gateway, MCP client Hermes docs. Notice MCP is a client feature: you point it at a URL. We are not in their tool catalog. You add social-by-idm yourself.

If your build supports an exclude list on MCP tools, exclude anything that publishes immediately from the cron profile. Otherwise default every write to draft: true in the prompt and in SOUL.md. Defense in depth.

How do you install the Social by InstantDM skill on Hermes?

hermes skills install github:instant-dm/agent-mode/skills/social-by-idm

GitHub source, not ClawHub. Hermes does not use ClawHub as its primary registry. The portable unit is the folder. We tell Hermes to clone the same SKILL.md OpenClaw gets from @sanjaykhanssk/social-by-idm. Install notes. Format: SKILL.md on OpenClaw and Hermes.

The skill tells Hermes:

  1. Stop if the API key is missing — do not hunt .env files, do not read config.yaml into a caption.
  2. list_accounts before any write.
  3. Prefer draft: true unless you said “publish now.”
  4. Put a timezone offset on scheduledAt.
  5. Call get_post_status after publish — HTTP 200 is not “all five networks live.”
  6. Never fake likes or duplicate the same caption+image across accounts.

It does not install a binary. It does not curl during install. If a lookalike skill runs shell in Prerequisites, abort.

If you migrated from OpenClaw, look in ~/.hermes/skills/openclaw-imports/. Two social skills will argue. Keep one. Pin the vendor skill:

# if your build has curator pin:
hermes curator pin social-by-idm

If the command does not exist yet on your version, the SOUL redundancy is the pin. Hermes’s loop will try to skill-ize “how we posted Tuesdays.” A publishing skill that drifts is how timezone offsets disappear.

What is the first prompt?

In CLI or Telegram:

List my connected accounts. Do not post.

You want real acc_… IDs, platforms, handles. If it asks you to paste a key in chat, you misconfigured env — and the skill should have refused to search disk. That refusal is load-bearing. Do not relax it.

Then:

Load the social-by-idm skill. Draft an X post and a LinkedIn post from these bullets: … Save as drafts. Do not publish.

Then you, in the calendar at social-app.instantdm.com. Hermes will try to be helpful and skip the review if SOUL.md is silent. Write it.

A list_accounts response the agent should read back:

{
  "accounts": [
    { "accountId": "acc_8fK2qz", "platform": "instagram", "handle": "@acme", "displayName": "Acme Inc" },
    { "accountId": "acc_p1L9wd", "platform": "linkedin", "handle": "acme-inc", "displayName": "Acme Inc" },
    { "accountId": "acc_x7n2ab", "platform": "twitter", "handle": "@acme", "displayName": "Acme" }
  ]
}

Empty array: OAuth is not done. Empty tool list: YAML, header, or you forgot /reload-mcp. Wrong IDs: the agent hallucinated acc_instagram_1. Always copy from list_accounts.

Social by InstantDM MCP page — the URL Hermes should call socialbyidm.com/mcp. Notice the Streamable HTTP URL and the 16-tool list. Paste that URL into config.yaml. Do not invent a /sse path or a localhost port.

What does a create_post payload look like from Hermes?

Same JSON the OpenClaw path sends. Hermes is the client. We are the server.

Draft only:

{
  "accountIds": ["acc_x7n2ab", "acc_p1L9wd"],
  "content": "Dark mode is on by default for new workspaces. Existing users: Settings → Appearance.",
  "draft": true
}

Scheduled with an offset — always an offset:

{
  "accountIds": ["acc_x7n2ab"],
  "content": "Dark mode is on by default for new workspaces.",
  "scheduledAt": "2026-09-16T09:00:00+05:30"
}

One idea, three captions via platformContent:

{
  "accountIds": ["acc_x7n2ab", "acc_p1L9wd", "acc_8fK2qz"],
  "content": "Dark mode is on by default for new workspaces.",
  "platformContent": {
    "twitter": "Dark mode is the default for new workspaces. Existing users: Settings → Appearance.",
    "linkedin": "We turned dark mode on by default for new workspaces.\n\nExisting users keep their current theme. The toggle is in Settings → Appearance.\n\nAgent tools did not change — this is a UI default.\n\nWhat else should default to on?",
    "instagram": "Dark mode, on by default.\n\nSettings → Appearance for older workspaces."
  },
  "draft": true
}

Staggered times via platformSchedules:

{
  "accountIds": ["acc_p1L9wd", "acc_8fK2qz", "acc_tk01"],
  "content": "Dark mode is on by default for new workspaces.",
  "scheduledAt": "2026-09-16T08:30:00-04:00",
  "platformSchedules": {
    "linkedin": "2026-09-16T08:30:00-04:00",
    "instagram": "2026-09-16T11:00:00-04:00",
    "tiktok": "2026-09-16T18:00:00-04:00"
  }
}

Timezone rule, the one Hermes will get wrong without the skill: no offset = workspace timezone, not UTC. Workspace TZ lives in Settings → Timezone and defaults to UTC. A naive 2026-09-16T09:00:00 is 09:00 in that zone. A past or near-now stamp (≤30 seconds) publishes immediately. Put +05:30 or Z on every value, including every platformSchedules entry.

Instagram still needs media. Upload first:

{
  "url": "https://cdn.example.com/dark-mode.png"
}

that is upload_media_from_url, which returns mediaId. Then:

{
  "accountIds": ["acc_8fK2qz"],
  "postType": "image",
  "content": "Dark mode, on by default.",
  "mediaIds": ["md_1a2b3c"],
  "draft": true
}

Carousel: two or more images. One image + postType: "carousel"422 post_type_invalid. Local file: create_upload_url with filename and contentType, PUT the bytes to uploadUrl, then complete_upload. Drive preview links are not media.

REST check from the same machine, to separate Hermes bugs from key bugs:

curl "https://social-api.instantdm.com/v1/accounts" \
  -H "X-Api-Key: $SOCIAL_BY_IDM_API_KEY"

If curl lists accounts and Hermes does not, /reload-mcp and check which user the gateway runs as.

How should Hermes cron look?

Native cron is why people try to make Hermes a social media manager. Keep the two clocks apart.

ClockOwnerExample
Agent wakehermes cronWeekdays 08:00 Asia/Kolkata: draft five posts
Network go-livescheduledAt / platformSchedulesLinkedIn 08:30 America/New_York, IG 11:00, TikTok 18:00

A prompt that has survived contact with real brands:

Isolated session. Read SOUL.md. Research today’s niche sources. Produce five drafts (IG, LinkedIn, X, TikTok, Facebook) for this week’s remaining empty slots. draft: true. Never publish_now. Deliver titles + post IDs to Telegram. Stop.

hermes cron create "0 8 * * 1-5" "Read SOUL.md. Draft five posts for empty slots this week. draft true. Never publish_now. Deliver IDs to Telegram." --name "weekday-drafts" --deliver telegram

--deliver telegram is the review surface. You open the calendar, or you reply with an approve-pack if you built one. Then the publisher clock starts. Always-on calendars.

What cron must not contain: “publish the week’s content,” “go live at 9,” “if the draft looks good just post it.” The model will decide it looks good. Isolated session matters — yesterday’s complaint about a customer should not become today’s LinkedIn caption.

If the VPS was down at 08:00, skip. Do not dump seven posts at 14:02. Catch-up policy belongs in the cron prompt: “If this run is more than 30 minutes late, skip and tell Telegram. Do not burst.”

Hermes also accepts intervals (every 2h) and natural language for cron. The schedule syntax is not the risk. The verb publish_now is the risk.

How do you know it actually posted?

get_post_status. Hermes likes to declare victory after HTTP 200. Status partial means Instagram made it and TikTok did not.

{
  "postId": "pst_3kqz1a",
  "status": "partial",
  "ready": true,
  "targets": [
    {
      "accountId": "acc_8fK2qz",
      "platform": "instagram",
      "status": "published",
      "permalink": "https://www.instagram.com/p/…",
      "error": null
    },
    {
      "accountId": "acc_tk01",
      "platform": "tiktok",
      "status": "failed",
      "permalink": null,
      "error": "Photo must be JPEG ≤1920px and <10MB"
    }
  ]
}

Tell Telegram the truth: “IG live, TikTok failed, reason: JPEG size.” Do not retry Instagram. Do not invent a permalink for TikTok.

Other states: draft, scheduled, publishing (ready: false while a carousel assembles), published, failed, canceled. Poll get_post_status after publishNow. Do not 50ms-loop it; 120 requests/minute per key, then 429 rate_limited.

update_post activates a draft when you pass scheduledAt or publishNow (response includes activated: true). A bare scheduledAt change moves all targets and clears platformSchedules. If you only wanted to nudge LinkedIn, send platformSchedules only.

LinkedIn personal delete cannot be done via API. delete_post will list it under manual. Hermes must not report “deleted everywhere” when the body says otherwise.

Social by InstantDM 2026 dashboard — where Hermes drafts should appear The 2026 app. After cron or a chat draft, this calendar is source of truth. If the row is missing, Hermes role-played a tool call. Check gateway logs for the actual JSON-RPC.

What are the 16 tools, with the scopes Hermes will need?

ToolScopeWhen Hermes should call it
list_accountsaccounts:readFirst. Always.
create_postposts:writeAfter IDs exist. draft: true unless you said otherwise.
update_postposts:writeActivate a draft, reschedule, fix a caption.
get_postposts:readFull object + targets.
get_post_statusposts:readAfter any live attempt. Partial is the real failure.
list_postsposts:read”What’s already queued this week?”
delete_postposts:writeCancel a scheduled post. Check manual.
upload_media_from_urlmedia:writePublic image/video URL → mediaId.
create_upload_urlmedia:writeLocal file, step 1.
complete_uploadmedia:writeLocal file, step 3 after PUT.
list_mediamedia:readReuse an asset.
delete_mediamedia:writeCleanup.
get_analyticsanalytics:readFriday note. Never invent numbers.
list_platform_postsposts:readNative posts, including ones not created here.
delete_platform_postposts:writeDelete by platform id where the API allows.
platform_requestplatform:proxyOpt-in proxy. Off on day one. X disabled.

Eight platforms: Instagram, Facebook, X, LinkedIn, TikTok, Threads, Pinterest, YouTube. Instagram rejects text-only. Threads is 500 characters. Pinterest wants a board and a link. YouTube is video only with youtubeTitle. Full rules live in /docs.

A thread payload Hermes should not guess:

{
  "accountIds": ["acc_x7n2ab"],
  "postType": "thread",
  "draft": true,
  "thread": [
    { "text": "1/ Dark mode is the default for new workspaces." },
    { "text": "2/ Existing users keep their theme." },
    { "text": "3/ MCP tools unchanged — UI only." }
  ]
}

What goes wrong — 401, 403, empty tools, empty accounts, agent publishes anyway?

SymptomLikely causeFix
Empty tools after editForgot /reload-mcp or gateway restart; YAML indent; enabled: falseReload. Validate YAML. Confirm enabled: true.
Empty tools, YAML looks rightHeader name (X-Api-Key vs Authorization: Bearer); key not in gateway envTry the other header. hermes doctor. Restart gateway from a shell that has .env.
401Key typo, revoked, space, quotes in .envRecopy. No quotes. Rotate only if it leaked.
403Missing scope (posts:write, accounts:read, media:write)Add the scope. Do not mint a new key first.
Tools present, empty accountsOAuth not connected; key is a different workspaceDashboard → Accounts. Check workspace on the key.
Agent publishes anywayLearning-loop homemade skill; SOUL silent; cron promptPin vendor skill. draft: true. Rewrite cron.
Agent asks for the key in chat.env not visible to the processFix env. Do not paste. Skill should refuse disk search.
422 content_invalidIG without media; caption too long; Threads > 500platformContent. Upload media first.
422 post_type_invalidCarousel with <2 imagesAttach ≥2 or use image.
404Hallucinated acc_ / pst_ / md_list_accounts / list_posts / list_media again.
409 publishingDelete mid-flightWait, retry.
429>120 req/minStop the loop.
Wrong hourNaive scheduledAtOffset on every stamp. Workspace TZ ≠ UTC unless you set UTC.
Two skills fightingopenclaw-imports/ plus GitHub installKeep one. Pin it.
Cron posted a stale statMemory had last week’s number; no get_analyticsSOUL: never invent metrics. Cron: drafts only.

401 body:

{ "error": { "code": "unauthorized", "message": "Missing or invalid API key." } }

403 body:

{ "error": { "code": "forbidden", "message": "Key lacks posts:write." } }

Hermes will sometimes “improve” a failed draft by retrying with publishNow: true. That is a model instinct, not a server feature. SOUL.md plus the skill plus a cron prompt that names draft: true three times is not overkill.

How do you stop Hermes “improving” the publisher?

The learning loop is the product. It is also the way a timezone rule dies.

After a week of successful drafts, Hermes may write ~/.hermes/skills/how-we-schedule-acme.md that encodes this week’s caption style, this week’s 9am Eastern, and a missing offset. Next month the product changed. The homemade skill did not. If it ranks hotter than social-by-idm, drafts start going live at 03:00.

What to do:

  1. Pin social-by-idm (hermes curator pin when available).
  2. Put “never publish_now unless the human said publish now” in SOUL.md. SOUL survives compaction. MEMORY.md does not.
  3. Let Hermes write helpers (“how we turn a changelog into five bullets”), not a second publisher.
  4. If you already have a homemade social skill, open it, grep for publishNow and naive timestamps, then delete or pin-disable it.

OpenClaw users make the inverse mistake: no SOUL, five ClawHub skills, LinkedIn-voice on TikTok. Hermes users make this one: one excellent self-authored skill that slowly lies.

NousResearch/hermes-agent on GitHub github.com/NousResearch/hermes-agent. Notice it is the runtime repo, MIT-licensed, not a scheduler. Stars measure curiosity. They do not measure whether your cron should publish_now.

How does this differ from OpenClaw?

Same 16 tools. Same payloads. Same “first call is list_accounts.” Different config and different personality.

OpenClawHermes
ConfigJSON, openclaw mcp addYAML mcp_servers
Skill installopenclaw skills install @sanjaykhanssk/social-by-idmhermes skills install github:instant-dm/agent-mode/skills/social-by-idm
CronAutomations / heartbeatBuilt-in cron with --deliver
RiskRandom ClawHub skillSelf-authored skill drift
iMessageYesNot the headline
Learning loopWeakerThe point

You can point both at one key if you like chaos. Better: one agent has posts:write, the other drafts in a text file. OpenClaw posting guide. OpenClaw mcp add.

Claude / ChatGPT / Codex use the same server with different menus — schedule from Claude or ChatGPT. Hermes YAML is closer to Codex’s config.toml than to Claude’s connector dialog.

How do you migrate from OpenClaw without doubling the publisher?

hermes claw migrate --dry-run
hermes claw migrate

Dry-run first. It copies SOUL.md, memories, skills, allowlisted API keys. Multi-agent OpenClaw setups are not a full clone. MCP JSON does not become YAML by magic. Re-add:

mcp_servers:
  social-by-idm:
    url: "https://social-api.instantdm.com/mcp"
    headers:
      X-Api-Key: "${SOCIAL_BY_IDM_API_KEY}"
    enabled: true

Imported skills land in ~/.hermes/skills/openclaw-imports/. If that folder contains a social skill and you also ran the GitHub install, delete one. Then /reload-mcp. Then the read-only prompt. Then a draft. Then look at the calendar.

Gateway bind: do not put Hermes on the public internet with a posting key. Same rule as OpenClaw’s port 18789. Tailscale, loopback, or a firewall you actually checked.

What should never be in a Hermes social setup?

  • publish_now inside hermes cron create
  • A homemade skill that omits offsets and drafts
  • The API key in Telegram, Discord, or a gist
  • Browser automation against Instagram web, or unofficial mobile APIs
  • The same caption+image on three IG accounts
  • Invented analytics (“we hit 10k likes”) that get_analytics did not return
  • platform:proxy on the weekday key
  • Two social skills, one of them from openclaw-imports/

Freeze line, practiced once:

Freeze social. Do not create, schedule, or publish. Confirm the queue is untouched.

Put it in SOUL.md. When a caption is wrong on a live network, freeze first, argue later.

What does a local media upload look like when Hermes is the client?

Public URLs are upload_media_from_url. Local files are three steps. Hermes will try to skip PUT.

# already in config.yaml — reminder, not a second server
mcp_servers:
  social-by-idm:
    url: "https://social-api.instantdm.com/mcp"
    headers:
      X-Api-Key: "${SOCIAL_BY_IDM_API_KEY}"
    enabled: true

Then the agent should:

  1. Call create_upload_url with filename and contentType (image/jpeg, video/mp4, application/pdf).
  2. PUT bytes to the returned uploadUrl. That PUT is HTTPS to object storage, not a second MCP call.
  3. Call complete_upload on the mediaId.
  4. Call create_post with mediaIds, draft: true, and platformContent if more than one network.

Prompt that forces the sequence:

Upload ./out/dark-mode.jpg via create_upload_url, PUT, complete_upload. Then draft an Instagram image post to acc_… with that mediaId. draft true. Do not publish. Return mediaId and postId.

If Hermes says “uploaded” but list_media shows pending, step 3 never happened. If create_post 404s the media, same. If Instagram 422 content_invalid, you attached nothing.

Windows note: the gateway’s cwd is not your Desktop. Use an absolute path or a URL. WSL2 and native Windows are different filesystems; a /mnt/c/... path the CLI sees may be invisible to a gateway running as a different user.

Platform limits the skill is reminding Hermes of, because the model will blow past them: Instagram 2,200 characters and media required; Threads 500; LinkedIn 3,000 and one media; TikTok JPEG ≤1920px; X 280 on the free tier; Pinterest board + link; YouTube youtubeTitle ≤ 100. platformContent exists so the LinkedIn essay does not become the X post.

How do you verify the whole path in five minutes?

  1. hermes doctor is clean.
  2. ~/.hermes/.env has SOCIAL_BY_IDM_API_KEY and mode 600.
  3. config.yaml has the mcp_servers block, enabled: true, URL exactly https://social-api.instantdm.com/mcp.
  4. /reload-mcp or gateway restart.
  5. “What social tools do you have?” → 16 names.
  6. “List my connected accounts. Do not post.” → real acc_ IDs.
  7. A draft prompt with draft: true.
  8. Calendar row exists. You read it. You schedule with an offset or you delete it.
  9. After any live publish: get_post_status, including partial.

Done is a draft in the calendar, not a live post. Hermes is the learner. We are the last mile. I sell that last mile because OAuth, media validators and per-target status are a product, not a SKILL.md. Connect them, pin the skill, keep cron on drafts.

Product: /agents · /mcp · docs. OpenClaw sibling: How to let OpenClaw post. Skill install: Hermes skill.