---
name: post-for-me
description: >
  Complete guide for posting to social media via the Post for Me MCP (`mcp__post_for_me__execute`). Use this skill whenever you need to create social media posts, upload images, check post status, list social accounts, manage media, or interact with Post for Me in any way. Trigger on: "post to X/Threads/Instagram/TikTok/LinkedIn/YouTube", "publish to social", "upload and post images", "check my social accounts", "did that post go through", "schedule a post", "post this to [platform]", "create a social post", "send this to social media", or any reference to Post for Me, social posting, or cross-platform publishing. Even if the user doesn't say "Post for Me" by name — if they want to publish content to social platforms, this is the skill to use.
---

# Post for Me — Complete Usage Guide

This skill teaches you how to use the Post for Me MCP correctly. Post for Me is a social media posting API that lets you publish text and media to X (Twitter), Threads, Instagram, TikTok, LinkedIn, YouTube, Facebook, Pinterest, and Bluesky — all through a single SDK.

The MCP tool is `mcp__post_for_me__execute`. You write TypeScript code that receives an initialized SDK `client` and runs against the Post for Me API. You also have `mcp__post_for_me__search_docs` to look up API details on the fly if you need specifics not covered here.

## The Golden Rule: Read Before You Post

Social media posts are irreversible. Once posted, you can't un-post. So the workflow is always:

1. **Read the content** — find and fully read every text file or data source that contains what you're about to post
2. **Check for duplicates** — query recent posts to make sure this exact content hasn't already been published
3. **Confirm with the user** — show them exactly what will be posted, to which platforms, and wait for a "yes"
4. **Post one platform at a time** — don't blast everything simultaneously; post sequentially so you can catch errors early
5. **Verify the result** — check that the post went through successfully before moving on

Breaking any of these steps has real consequences: duplicate posts look unprofessional, wrong content gets seen by thousands of followers, and failed posts that aren't caught leave gaps in a publishing schedule.

## How the MCP Tool Works

Every interaction goes through `mcp__post_for_me__execute`. You write an `async function run(client)` that uses the SDK client. The function can `return` data or `console.log()` it — both come back to you.

```typescript
async function run(client) {
  // Your SDK calls here
  const result = await client.socialPosts.create({ ... });
  return result;
}
```

Variables don't persist between calls. If you need data from one call in a later call, return it and pass it into the next execution explicitly.

---

## Before Posting: Prepare Your Content

### Step 1 — Find and Read Content Files

When the user asks you to post something, your first job is to find the actual content. Typically this means local text files. Read them in full — don't skim, don't truncate, don't assume you know what's in them.

If the user points you at a folder, use the file tools (Read, Glob) to find `.txt` files and read each one completely. Content files often have numbered sections like:

```
1. Final X Post 1
[the full post text]

2. Final X Post 2
[the full post text]
...
```

Read every section. The user wrote this content carefully and expects it posted verbatim. Don't paraphrase, don't "improve" the text, don't fix grammar unless explicitly asked. Post exactly what's in the file.

If you're unsure which file maps to which platform, ask the user rather than guessing. Common patterns:
- Files with "short form" in the name → X, Threads, LinkedIn text posts
- Files with "visual social" in the name → Instagram captions, TikTok captions
- Files with "long form" in the name → blog posts, newsletters (not social)

### Step 2 — Clean the Text (Light Touch Only)

Before posting, scan the content for these common issues:
- **Em dashes (—)** — some platforms don't render these well. If you spot them, flag it to the user but don't auto-replace unless told to.
- **Smart quotes (" ")** — same issue. Flag, don't auto-fix.
- **Trailing whitespace or extra newlines** — trim these silently, they're just formatting noise.
- **Template literal indentation** — CRITICAL: When writing caption text inside template literals (backticks), keep ALL lines flush-left with zero indentation. Template literals preserve whitespace exactly as written, so any code indentation bleeds into the posted text as visible leading spaces. Never indent continuation lines of a caption to match surrounding code formatting. Use a separate variable if needed: `const caption = \`line one\nline two\`.trim();`
- **Character limits** — X has a 280 character limit (or 25,000 for Premium). If a post exceeds the platform limit, tell the user before posting.

---

## Duplicate Prevention

This is critical. Posting the same thing twice makes the user look spammy and unprofessional. Before every single post creation, check for duplicates.

### How to Check

Query recent posts filtered by the specific social account you're about to post to:

```typescript
async function run(client) {
  const recent = await client.socialPosts.list({
    social_account_id: ['THE_ACCOUNT_ID'],
    limit: 20
  });

  // Log captions so you can compare
  const posts = recent.data.map(p => ({
    id: p.id,
    caption: p.caption?.substring(0, 80),
    status: p.status,
    created_at: p.created_at
  }));
  return posts;
}
```

Then compare the caption you're about to post against what's already there. A match doesn't have to be exact — if the first 50 characters are identical, it's almost certainly a duplicate.

### What to Do When You Find a Duplicate

Tell the user clearly:

> "I found an existing post with the same caption on this account (post ID: sp_xxxxx, status: processed). This looks like a duplicate. Should I skip this one, or do you want me to post it anyway?"

Never silently skip a duplicate and never silently post a duplicate. Always surface it.

### Processing Posts

When you create a post, it returns with `status: "processing"`. This means Post for Me is sending it to the platform. Don't fire another post to the same account while one is still processing — wait and check:

```typescript
async function run(client) {
  const post = await client.socialPosts.retrieve('THE_POST_ID');
  return { id: post.id, status: post.status };
}
```

A post moves from `processing` → `processed` when done. If it stays in `processing` for more than 60 seconds, something may be wrong. Check the results endpoint for error details (see "Checking Post Results" below).

---

## Core API Reference

### Social Accounts

These are the connected social media accounts (X, Instagram, TikTok, etc.). Each has a unique ID like `spc_xxxxx`.

**List all accounts:**
```typescript
async function run(client) {
  const accounts = await client.socialAccounts.list();
  return accounts.data.map(a => ({
    id: a.id,
    platform: a.platform,
    username: a.username,
    status: a.status
  }));
}
```

**Filter by platform or status:**
```typescript
async function run(client) {
  const accounts = await client.socialAccounts.list({
    platform: ['x', 'threads'],
    status: ['connected']
  });
  return accounts.data;
}
```

Before posting, always verify the account is `status: "connected"`. Posting to a disconnected account will fail. If you find a disconnected account, tell the user they need to reconnect it at postforme.dev → Social Accounts.

TikTok accounts are especially prone to disconnection because their tokens expire every ~24 hours. If a TikTok post fails, the first thing to check is the account status.

**Get a specific account:**
```typescript
async function run(client) {
  const account = await client.socialAccounts.retrieve('spc_xxxxx');
  return { id: account.id, platform: account.platform, status: account.status, username: account.username };
}
```

### Creating Posts

**Text-only post (X, Threads, LinkedIn):**
```typescript
async function run(client) {
  const post = await client.socialPosts.create({
    caption: 'Your post text here',
    social_accounts: ['spc_xxxxx']
  });
  return { id: post.id, status: post.status };
}
```

**Post with images (Instagram carousel, etc.):**
```typescript
async function run(client) {
  const post = await client.socialPosts.create({
    caption: 'Caption text',
    social_accounts: ['spc_xxxxx'],
    media: [
      { url: 'https://public-url-to-image-1.jpg' },
      { url: 'https://public-url-to-image-2.jpg' }
    ]
  });
  return { id: post.id, status: post.status };
}
```

Media URLs must be publicly accessible HTTPS URLs. If your images are local files, you need to upload them first (see "Uploading Media" below).

**Post to multiple accounts at once:**
```typescript
async function run(client) {
  const post = await client.socialPosts.create({
    caption: 'Same caption for all',
    social_accounts: ['spc_account1', 'spc_account2', 'spc_account3']
  });
  return { id: post.id, status: post.status };
}
```

This creates one post object that gets sent to all listed accounts. Use this when you want the exact same content on multiple platforms simultaneously. If different platforms need different captions, use `account_configurations` to override per account:

```typescript
async function run(client) {
  const post = await client.socialPosts.create({
    caption: 'Default caption for most platforms',
    social_accounts: ['spc_x_account', 'spc_tiktok_account'],
    account_configurations: [
      {
        social_account_id: 'spc_tiktok_account',
        configuration: {
          caption: 'Different caption for TikTok with #hashtags',
          privacy_status: 'public'
        }
      }
    ]
  });
  return { id: post.id, status: post.status };
}
```

**Scheduling a post for later:**
```typescript
async function run(client) {
  const post = await client.socialPosts.create({
    caption: 'This posts tomorrow at 9am',
    social_accounts: ['spc_xxxxx'],
    scheduled_at: '2026-03-31T09:00:00Z'  // ISO 8601 format
  });
  return { id: post.id, status: post.status, scheduled_at: post.scheduled_at };
}
```

Setting `scheduled_at` to null or omitting it means the post goes out immediately.

**Using external_id for tracking:**

You can set an `external_id` on a post to tag it with your own identifier. This is great for duplicate prevention — you can filter posts by external_id later:

```typescript
async function run(client) {
  // Check if we already posted this
  const existing = await client.socialPosts.list({
    external_id: ['my-unique-content-id-2026-03-30']
  });
  if (existing.data.length > 0) {
    return { skipped: true, reason: 'Already posted', existing_id: existing.data[0].id };
  }

  const post = await client.socialPosts.create({
    caption: 'The post content',
    social_accounts: ['spc_xxxxx'],
    external_id: 'my-unique-content-id-2026-03-30'
  });
  return { id: post.id, status: post.status };
}
```

### Platform-Specific Options

**TikTok:**
- `privacy_status`: `"public"` | `"private"` — defaults vary by account
- `allow_comment`, `allow_duet`, `allow_stitch`: boolean toggles
- `auto_add_music`: auto-adds music to photo posts on TikTok
- `is_ai_generated`: flags content as AI generated
- `is_draft`: creates a draft that must be published from the TikTok app
- `disclose_branded_content`, `disclose_your_brand`: brand disclosure toggles

**Instagram:**
- `placement`: `"reels"` | `"timeline"` | `"stories"` — where the post appears
- `share_to_feed`: if false, video posts only show in Reels tab
- `trial_reel_type`: `"manual"` | `"performance"` — creates as a trial reel
- `collaborators`: invite collaborators to a Reel

**YouTube:**
- `privacy_status`: `"public"` | `"private"` | `"unlisted"`
- `made_for_kids`: boolean, defaults to false
- Use `title` in account_configurations to set the video title

**X (Twitter):**
- `reply_settings`: `"following"` | `"mentionedUsers"` | `"subscribers"` | `"verified"`
- `quote_tweet_id`: ID of tweet to quote
- `community_id`: ID of a Twitter community to post to
- `poll`: `{ duration_minutes: number, options: string[] }` — create a poll (2-4 options)

**LinkedIn:**
- No special configuration options beyond the standard fields

**Threads:**
- `placement`: `"timeline"` (standard)

### Listing and Retrieving Posts

**List recent posts:**
```typescript
async function run(client) {
  const posts = await client.socialPosts.list({ limit: 10 });
  return posts.data.map(p => ({
    id: p.id,
    caption: p.caption?.substring(0, 60),
    status: p.status,
    created_at: p.created_at,
    platforms: p.social_accounts?.map(a => a.platform)
  }));
}
```

**Filter by account or platform:**
```typescript
async function run(client) {
  const posts = await client.socialPosts.list({
    social_account_id: ['spc_xxxxx'],
    status: ['processing', 'processed'],
    limit: 5
  });
  return posts.data;
}
```

Available filters: `platform`, `status` (draft/scheduled/processing/processed), `external_id`, `social_account_id`, `offset`, `limit`.

**Get a specific post:**
```typescript
async function run(client) {
  const post = await client.socialPosts.retrieve('sp_xxxxx');
  return post;
}
```

### Checking Post Results

After a post is processed, you can see per-account results — whether each platform post succeeded or failed:

```typescript
async function run(client) {
  const results = await client.socialPostResults.list({
    post_id: ['sp_xxxxx']
  });
  return results.data.map(r => ({
    social_account_id: r.social_account_id,
    success: r.success,
    platform_url: r.platform_data?.url,
    platform_id: r.platform_data?.id,
    error: r.error
  }));
}
```

This is how you verify a post actually went through. The `platform_data.url` gives you the direct link to the published post. The `error` field tells you what went wrong if `success` is false.

**Get a specific result:**
```typescript
async function run(client) {
  const result = await client.socialPostResults.retrieve('spr_xxxxx');
  return result;
}
```

### Social Account Feeds

View what's been posted to a specific account (the feed from the platform's perspective):

```typescript
async function run(client) {
  const feed = await client.socialAccountFeeds.list('spc_xxxxx', {
    limit: 10,
    expand: ['metrics']  // include engagement metrics
  });
  return feed.data.map(p => ({
    caption: p.caption?.substring(0, 60),
    platform_post_id: p.platform_post_id,
    platform_url: p.platform_url,
    posted_at: p.posted_at,
    metrics: p.metrics
  }));
}
```

The `expand: ['metrics']` option pulls in engagement data (likes, comments, views, shares, etc.) from the platform. This is useful for checking how posts are performing.

### Uploading Media

If you have local image files that need to be attached to a post, you need public URLs. Post for Me provides a two-step upload flow:

**Step 1 — Get an upload URL:**
```typescript
async function run(client) {
  const upload = await client.media.createUploadURL();
  return {
    media_url: upload.media_url,    // Use this in your post
    upload_url: upload.upload_url   // PUT your file binary here
  };
}
```

**Step 2 — Upload the file to the signed URL:**

The `upload_url` is a temporary signed URL. You PUT the raw file bytes to it. This step happens outside the MCP — you'd use `fetch` or `curl` from the local environment:

```bash
curl -X PUT "THE_UPLOAD_URL" \
  -H "Content-Type: image/png" \
  --data-binary @/path/to/image.png
```

**Step 3 — Use `media_url` in your post:**

After uploading, use the `media_url` (not the `upload_url`) as the image URL in your `socialPosts.create()` call.

The upload URL expires quickly (minutes, not hours), so upload promptly after requesting it. If you're uploading multiple images (e.g., for an Instagram carousel), request one upload URL per image and upload them all before creating the post.

**Important:** The MCP `execute` tool runs in a container that can't access external networks directly. If you need to upload local files, you'll need to do the PUT step from the local machine (via Bash/curl), not from within the `mcp__post_for_me__execute` call.

### Updating and Deleting Posts

**Update a post** (only works if status is `draft` or `scheduled` — you can't update a post that's already been sent):
```typescript
async function run(client) {
  const updated = await client.socialPosts.update('sp_xxxxx', {
    caption: 'Updated caption text',
    social_accounts: ['spc_xxxxx']
  });
  return { id: updated.id, status: updated.status };
}
```

**Delete a post:**
```typescript
async function run(client) {
  const result = await client.socialPosts.delete('sp_xxxxx');
  return result;  // { success: true }
}
```

---

## Recommended Posting Workflow

Here's the full workflow to follow when the user asks you to post content:

### 1. Locate the content
- Ask the user where the content is (or look in the folder they point to)
- Read every relevant text file in full using the Read tool
- Identify which text maps to which platform

### 2. Check account status
```typescript
async function run(client) {
  const accounts = await client.socialAccounts.list({ status: ['connected'] });
  return accounts.data.map(a => ({ id: a.id, platform: a.platform, username: a.username }));
}
```
- Confirm the target accounts are connected
- If an account is disconnected, tell the user before proceeding

### 3. Check for duplicates
- For each account you plan to post to, list recent posts and compare captions
- If you find a match, stop and ask the user

### 4. Show the user what you're about to post
- Display the exact text for each platform
- Show which account (by username) each post targets
- List any media that will be attached
- Wait for explicit confirmation ("yes", "go", "post it")

### 5. Post sequentially
- Post to one platform at a time
- After each `create()` call, check the returned status
- If status is `processing`, wait a few seconds then check again with `retrieve()`
- Log the post ID for each successful post
- If any post fails, stop and report the error before continuing to the next platform

### 6. Verify results
- After all posts are created, check `socialPostResults.list()` for each post ID
- Report success/failure for each platform with the platform URL if available
- If any failed, provide the error details and ask the user how to proceed

---

## Known Limitations

Post For Me does NOT support these features. Do not attempt them, do not search for workarounds, do not suggest them to the user. If the user asks for any of these, tell them immediately that the API doesn't support it.

**Cannot do:**
- **Replies**: No reply-to functionality on any platform. Cannot reply under an existing post, comment on a post, or create a thread by replying to yourself.
- **Edit published posts**: Once a post is `processed`, it cannot be modified through the API. Only `draft` and `scheduled` posts can be updated.
- **Retweets/Reposts**: Cannot retweet or repost. Can only quote tweet on X (via `quote_tweet_id`). No equivalent for Threads, Instagram, or other platforms.
- **Delete from platform**: `socialPosts.delete()` deletes the Post For Me record, not the actual post on the platform. The post remains live on X/Threads/etc.
- **Visual preview**: Cannot show the user what the post will look like on the platform before publishing. Text-only confirmation is all you get.
- **Direct message**: Cannot send DMs on any platform.
- **Analytics deep dive**: `socialAccountFeeds.list()` with `expand: ['metrics']` provides basic engagement numbers but not detailed analytics like impressions over time or audience demographics.

---

## Multi-Platform Content Differences

When posting the same content to multiple platforms, do NOT blindly send identical text. Check for these platform-specific issues and adapt:

**@ mentions**: Platform-specific. `@username` on X is not the same person on Threads or Instagram. If a post tags someone on X, check whether those people exist on other platforms. If unsure, remove the mentions for other platforms rather than tagging wrong accounts or leaving broken references.

**Hashtags**: Standard on Instagram and TikTok, less common on X and LinkedIn. Don't add hashtags to X posts unless the user included them. Don't strip them from Instagram posts.

**Links**: X renders link previews. Threads and Instagram do not make links clickable in post text. If a post relies on a clickable link, warn the user it won't work on Threads/Instagram.

**Character limits**: X is 280 (or 25,000 for Premium). Threads is 500. LinkedIn is 3,000. If content exceeds a platform's limit, tell the user before posting.

**Post structure**: What works as a single post on one platform might need to be split or condensed for another. Flag this rather than silently modifying.

When posting to multiple platforms with different content, use `account_configurations` to override per-account:

```typescript
async function run(client) {
const caption = [
'Default caption for most platforms'
].join('\n');

const post = await client.socialPosts.create({
caption,
social_accounts: ['spc_x_account', 'spc_threads_account'],
account_configurations: [
  {
    social_account_id: 'spc_threads_account',
    configuration: {
      caption: 'Adapted caption without @ mentions for Threads'
    }
  }
]
});
return { id: post.id, status: post.status };
}
```

Alternatively, create separate posts per platform when content differs significantly. This gives cleaner tracking and independent error handling.

---

## Caption Construction Safety

**CRITICAL: NEVER use template literals (backticks) for captions. This has caused live post damage. Use explicit `\n` strings ONLY.**

Template literals preserve whitespace from code indentation. Even when text looks flush-left in your editor, the tool parameter serialization can inject leading spaces. This bug is invisible until the post goes live and cannot be fixed after publishing.

**The ONLY safe pattern:**
```typescript
async function run(client) {
  const caption = 'First paragraph here.\n\nSecond paragraph here.\n\nThird paragraph here.';

  const post = await client.socialPosts.create({
    caption: caption.trim(),
    social_accounts: ['spc_xxxxx']
  });
  return { id: post.id, status: post.status };
}
```

**NEVER do this** (any variation of template literals):
```typescript
// WRONG — template literal, will inject spaces
const caption = `First paragraph.

Second paragraph.`;

// WRONG — even flush-left template literals are unsafe
const caption = `First paragraph.
Second paragraph.`;
```

**Rules:**
1. Always use single-quoted strings with explicit `\n` for line breaks
2. Always call `.trim()` on the caption before passing to create()
3. Never use backticks for caption content under any circumstances
4. When building long captions, use string concatenation or array join:

```typescript
const caption = [
  'First paragraph here.',
  '',
  'Second paragraph here.',
  '',
  'Third paragraph here.'
].join('\n');
```

---

## Post Verification Timing

After creating a post, it enters `processing` status. Different platforms take different amounts of time:

- **X**: Usually processes in under 5 seconds
- **Threads**: Can take 10-30 seconds. Do not panic if results are empty on first check.
- **Instagram/TikTok**: Can take 30-60 seconds, especially with media
- **YouTube**: Can take several minutes for video processing

**Verification workflow:**
1. After `create()`, wait 5 seconds before the first check
2. Check with `socialPostResults.list({ post_id: ['sp_xxxxx'] })`
3. If results array is empty, the platform hasn't finished yet. Wait another 10 seconds and check again.
4. If status is `processed` but results are still empty, try one more time after 10 seconds
5. After 3 checks with no results, flag it to the user rather than keep polling

Do not rapid-fire result checks. Each check is an API call.

---

## Error Handling

**Token expired (especially TikTok):**
The post will fail with an auth error. Tell the user: "The TikTok account token has expired. You'll need to reconnect it at postforme.dev → Social Accounts, then we can try again."

**Media processing failure:**
If a post with images fails, it could be a format or size issue. Try `skip_processing: true` on the media item for large files — this sends the file as-is without Post for Me processing it, which works if the file already meets the platform's requirements.

**Rate limiting:**
If you get rate limit errors, wait 30 seconds and try again. Don't retry more than 3 times.

**Post stuck in processing:**
If a post stays in `processing` for over 2 minutes, check the results endpoint. If no result exists yet, it may still be working. If there's a result with `success: false`, the post failed — read the error message.

**Never retry automatically without telling the user.** Failed posts should be surfaced, not silently retried.

---

## Quick Reference

| SDK Method | What It Does |
|---|---|
| `client.socialAccounts.list(query?)` | List connected accounts (filter by platform, status) |
| `client.socialAccounts.retrieve(id)` | Get one account by ID |
| `client.socialPosts.create(body)` | Create a new post |
| `client.socialPosts.list(query?)` | List posts (filter by status, account, external_id) |
| `client.socialPosts.retrieve(id)` | Get one post by ID |
| `client.socialPosts.update(id, body)` | Update a draft/scheduled post |
| `client.socialPosts.delete(id)` | Delete a post |
| `client.socialPostResults.list(query?)` | Get per-platform results for posts |
| `client.socialPostResults.retrieve(id)` | Get one result by ID |
| `client.socialAccountFeeds.list(accountId, query?)` | View platform feed + metrics |
| `client.media.createUploadURL()` | Get signed URL for image upload |

| Post Status | Meaning |
|---|---|
| `draft` | Created but not being sent (set `isDraft: true`) |
| `scheduled` | Will be sent at `scheduled_at` time |
| `processing` | Currently being sent to platform(s) |
| `processed` | Done — check results for success/failure per account |

| Account Status | Meaning |
|---|---|
| `connected` | Ready to post |
| `disconnected` | Token expired or revoked — needs reconnection at postforme.dev |
