<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Eugeniya Ivanova</title>
    <description>The latest articles on DEV Community by Eugeniya Ivanova (@eugeniya_ivanova_4a58eadc).</description>
    <link>https://dev.to/eugeniya_ivanova_4a58eadc</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4017574%2F67a20934-fb30-41ef-bfcc-79a85f515c48.jpg</url>
      <title>DEV Community: Eugeniya Ivanova</title>
      <link>https://dev.to/eugeniya_ivanova_4a58eadc</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/eugeniya_ivanova_4a58eadc"/>
    <language>en</language>
    <item>
      <title>I tried to build an Apple Shortcut from code. Apple said no, four times.</title>
      <dc:creator>Eugeniya Ivanova</dc:creator>
      <pubDate>Mon, 27 Jul 2026 10:32:16 +0000</pubDate>
      <link>https://dev.to/eugeniya_ivanova_4a58eadc/i-tried-to-build-an-apple-shortcut-from-code-apple-said-no-four-times-4l5d</link>
      <guid>https://dev.to/eugeniya_ivanova_4a58eadc/i-tried-to-build-an-apple-shortcut-from-code-apple-said-no-four-times-4l5d</guid>
      <description>&lt;p&gt;We have a REST API. Apple has Shortcuts, an automation app that can send HTTP requests. Wiring one to the other looked like a free win: "Hey Siri, publish this" → POST to our API → done. No backend, no OAuth, no app to install.&lt;/p&gt;

&lt;p&gt;It works. It's on my phone. What I couldn't do — and this is where it got interesting — was build, edit, version, and ship that shortcut from code. Here are the walls I hit, with the exact errors, so you can decide whether to spend an evening the way I spent mine.&lt;/p&gt;

&lt;h2&gt;
  
  
  What works
&lt;/h2&gt;

&lt;p&gt;The shortcut itself is trivial, three steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Ask for Input (text) → "What are we posting?"&lt;/li&gt;
&lt;li&gt;Get Contents of URL → POST to &lt;code&gt;https://api.publora.com/api/v1/create-post&lt;/code&gt;, header &lt;code&gt;x-publora-key: &amp;lt;key&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Request body (JSON): &lt;code&gt;content&lt;/code&gt; from step 1, &lt;code&gt;platforms&lt;/code&gt; as an array with one connection ID&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Run it, Siri asks, you dictate, the post goes out. Here's the curl equivalent, to show how little logic there is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.publora.com/api/v1/create-post &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"x-publora-key: YOUR_API_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"content":"Shipped from Siri","platforms":["linkedin-ABC123"]}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Leave out &lt;code&gt;scheduledTime&lt;/code&gt; and the post is created as a draft, which is what you want while testing: nothing goes to a live audience.&lt;/p&gt;

&lt;p&gt;Building it on the phone took about forty minutes, most of that spent hunting for the right actions in a localized UI. Building it from code took forever. Here's why.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wall 1: you can't hand-write a &lt;code&gt;.shortcut&lt;/code&gt; file
&lt;/h2&gt;

&lt;p&gt;A shortcut on disk is a property list: &lt;code&gt;WFWorkflowActions&lt;/code&gt; is an array of dictionaries, each with an action identifier and its parameters. That part is easy to write by hand.&lt;/p&gt;

&lt;p&gt;The file has to be signed before anything will open it, and macOS ships a CLI for that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;shortcuts sign &lt;span class="nt"&gt;--mode&lt;/span&gt; anyone &lt;span class="nt"&gt;--input&lt;/span&gt; mine.plist &lt;span class="nt"&gt;--output&lt;/span&gt; mine.shortcut
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It rejects hand-written plists. XML, binary, a minimal one-action file, a full set of top-level keys, both signing modes — every time, the same "isn't in the correct format." &lt;code&gt;sign&lt;/code&gt; appears to accept only files the app itself produced.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wall 2: &lt;code&gt;sign&lt;/code&gt; doesn't work on a real file either
&lt;/h2&gt;

&lt;p&gt;Fine — I got a real one (trick below) and tried again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;shortcuts sign --mode anyone --input real.shortcut --output out.shortcut
Error: In order to do this, you must be signed into iCloud.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I am signed into iCloud. iCloud Drive is on, the &lt;code&gt;~/Library/Mobile Documents/com~apple~CloudDocs&lt;/code&gt; folder is there, &lt;code&gt;defaults read MobileMeAccounts&lt;/code&gt; shows the account. I tried it as a normal user process, through &lt;code&gt;launchctl asuser&lt;/code&gt;, and in a real Terminal.app GUI session. Same error each time. On macOS 14.6, &lt;code&gt;shortcuts sign&lt;/code&gt; seems to be simply broken. It may behave differently on other versions, but on 14.6 it's dead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wall 3: the import URL scheme only trusts iCloud
&lt;/h2&gt;

&lt;p&gt;There's a URL scheme to import a shortcut from a link:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;shortcuts://import-shortcut?url=&amp;lt;url&amp;gt;&amp;amp;name=Publora
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Serve the file over local HTTP → "Import failed: the specified shortcut URL is invalid." Serve it over real HTTPS (I tried a gist) → same error. The scheme seems to accept only &lt;code&gt;icloud.com/shortcuts/...&lt;/code&gt; links, i.e. files Apple itself signed. There's no side door.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wall 4: you can't even read what's already installed
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;shortcuts list&lt;/code&gt; happily prints my shortcuts, so the data exists somewhere. But the group containers are empty shells, and the only SQLite files in them are four-kilobyte stubs from a migration folder. Nothing to read, nothing to use as a template.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one thing that worked: pulling a shortcut out of an iCloud link
&lt;/h2&gt;

&lt;p&gt;When someone shares a shortcut with you, the link is backed by an undocumented endpoint that returns the actual file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"https://www.icloud.com/shortcuts/api/records/&amp;lt;share-id&amp;gt;"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; rec.json
&lt;span class="c"&gt;# fields.name.value           → shortcut name&lt;/span&gt;
&lt;span class="c"&gt;# fields.shortcut.value       → unsigned plist (readable, editable)&lt;/span&gt;
&lt;span class="c"&gt;# fields.signedShortcut.value → Apple-signed blob&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One catch: the &lt;code&gt;downloadURL&lt;/code&gt; contains a literal &lt;code&gt;${f}&lt;/code&gt; placeholder you have to substitute before the download resolves.&lt;/p&gt;

&lt;p&gt;Note: the values in the snippets below are anonymized — the IDs, keys, and parts of the endpoint response are not real. The point is that this path exists and that things travel out with the link, not a working recipe against other people's shortcuts. Exact behavior depends on the specific shortcut, so there's nothing to reproduce byte for byte here.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;python3&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;plistlib&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;
&lt;span class="n"&gt;rec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rec.json&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rec&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;fields&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;shortcut&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;downloadURL&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;${f}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;file&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;raw.shortcut&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;wb&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plistlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;raw.shortcut&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;WFWorkflowActions&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;EOF&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the shortcut can be read, diffed, and rewritten programmatically. You just can't push it back. This road is read-only.&lt;/p&gt;

&lt;h2&gt;
  
  
  The security part nobody warns you about
&lt;/h2&gt;

&lt;p&gt;Look at that field list again. Everything baked into a shortcut travels with the link, and the link is public to anyone who has it. In my first working version, the API key was written straight into the header field. So for a while, my key was one URL away from anyone I sent the shortcut to.&lt;/p&gt;

&lt;p&gt;That's the real lesson, and it isn't about reading other people's shortcuts — it's about what you hand out without noticing. If you're going to share a shortcut that talks to an authenticated API:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Never bake in a credential. Ask for it at runtime, or use Apple's import questions so the installer fills it in themselves.&lt;/li&gt;
&lt;li&gt;Don't make the user hunt for internal IDs by hand. Ours calls &lt;code&gt;GET /api/v1/platform-connections&lt;/code&gt; and reads the &lt;code&gt;platformId&lt;/code&gt; itself.&lt;/li&gt;
&lt;li&gt;When you're done testing, delete the iCloud link (Share → Remove iCloud Link) and rotate anything that may have leaked.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What this means if you were hoping to ship one
&lt;/h2&gt;

&lt;p&gt;No build step. Shortcuts are GUI artifacts. No CI, no review, no &lt;code&gt;git diff&lt;/code&gt; on a change.&lt;/p&gt;

&lt;p&gt;No programmatic distribution. The installable artifact is an iCloud link, and only the app can produce it.&lt;/p&gt;

&lt;p&gt;No store. Apple curates its own gallery inside the app, with no submission form. The real shelf is community catalogs like RoutineHub.&lt;/p&gt;

&lt;p&gt;A per-user key is a hard ceiling. Anyone unwilling to paste a key won't install your shortcut, however nice the flow is.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we shipped instead
&lt;/h2&gt;

&lt;p&gt;The thing I actually wanted — tell an assistant, and the post goes out — turned out not to need Apple at all. Our MCP server (&lt;code&gt;mcp.publora.com&lt;/code&gt;) does it in Claude and Cursor today, with no install and no key typed into a text field on a phone screen. And even the auth header is different there — MCP wants &lt;code&gt;Authorization: Bearer&lt;/code&gt;, not the REST &lt;code&gt;x-publora-key&lt;/code&gt;. No walls on that path.&lt;/p&gt;

&lt;p&gt;I don't regret the evening. I now know exactly where the wall is, and I can read any shortcut someone sends me. Not enough to ship one. Enough to post.&lt;/p&gt;

&lt;p&gt;Have you hit a wall trying to automate Shortcuts for real? Which one?&lt;/p&gt;

</description>
      <category>apple</category>
      <category>automation</category>
      <category>api</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Chrome Rejected Us Twice. Firefox Approved Instantly. Edge Is Still Thinking</title>
      <dc:creator>Eugeniya Ivanova</dc:creator>
      <pubDate>Wed, 22 Jul 2026 09:26:07 +0000</pubDate>
      <link>https://dev.to/eugeniya_ivanova_4a58eadc/chrome-rejected-us-twice-firefox-approved-instantly-edge-is-still-thinking-e0e</link>
      <guid>https://dev.to/eugeniya_ivanova_4a58eadc/chrome-rejected-us-twice-firefox-approved-instantly-edge-is-still-thinking-e0e</guid>
      <description>&lt;p&gt;You have a few extensions installed right now, and you probably can't name them all. An ad blocker, a password manager, something you installed three years ago for one specific task that has been quietly living there ever since. That's normal: the average Chrome user has eight to twelve installed and actively uses two or three.&lt;/p&gt;

&lt;p&gt;The whole point of an extension is to remove a context switch. Instead of opening a new tab, finding the service, logging in, and pasting the link, you get one icon right where you already are. The value is in skipping those five little steps, which is why extensions can look trivial until you start using one.&lt;/p&gt;

&lt;p&gt;That was my situation. I write articles and then share them across social accounts: finish the piece, open our product in a new tab, copy the link, pick the accounts, send. Five context switches. Enough that I'd often leave it for later.&lt;/p&gt;

&lt;p&gt;I wanted one button, so we built an extension. Click the icon and it pulls in the URL of the page you're on. You write your post and send it to the accounts you've picked without leaving the tab. The whole thing weighs seventeen kilobytes.&lt;/p&gt;

&lt;p&gt;For scale: the Chrome Web Store held 283,585 extensions as of July 18, 2026, against 30,509 in Edge in mid-June and roughly 83,000 in Firefox earlier this year. Chrome is nearly ten times the size of Edge, which matters later. Meanwhile, extension trackers put the median at about eighteen users, with seventy percent sitting at a hundred or fewer. Everyone sees the giants, but a typical extension is used by a few dozen people.&lt;/p&gt;

&lt;p&gt;Launching wasn't the interesting part. Over about two weeks, we went through three stores and collected two rejections and one HTTP 400 that locked out half the team. That's what this article is about, along with the actual wording we got.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's inside
&lt;/h2&gt;

&lt;p&gt;Manifest V3, a popup, and a small amount of logic. The extension reads a token from the logged-in tab of our product, exchanges it for an API key, and stores it in &lt;code&gt;chrome.storage&lt;/code&gt;, so nobody has to copy anything by hand.&lt;/p&gt;

&lt;p&gt;Here's the manifest we ended up with:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;{&lt;br&gt;
"manifest_version": 3,&lt;br&gt;
"name": "...",&lt;br&gt;
"version": "0.2.3",&lt;br&gt;
"permissions": ["storage"],&lt;br&gt;
"action": { "default_popup": "popup.html" }&lt;br&gt;
}&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;More interesting is what's missing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chrome turned us down twice
&lt;/h2&gt;

&lt;p&gt;The first rejection came back as &lt;strong&gt;Spam and Placement / excessive keywords&lt;/strong&gt;. In the description, we had listed all ten platforms the extension works with — LinkedIn, X, Instagram, and the rest — so readers would know where they could post. The automated filter read it differently.&lt;/p&gt;

&lt;p&gt;Chrome's policy defines keyword spam as irrelevant or excessive keywords in a description used to manipulate ranking. On paper, we fit the definition: ten recognizable brand names in a row inside a short description look like an attempt to influence search, regardless of intent.&lt;/p&gt;

&lt;p&gt;When your product genuinely supports ten networks, an honest list of them can look exactly like keyword spam. We fixed it by generalizing: "all your connected social accounts" instead of the full list, with no brand names at all.&lt;/p&gt;

&lt;p&gt;The second rejection arrived as &lt;strong&gt;Use of Permissions&lt;/strong&gt;. We had requested &lt;code&gt;tabs&lt;/code&gt; but only ever called &lt;code&gt;chrome.tabs.create&lt;/code&gt;, which doesn't require the &lt;code&gt;tabs&lt;/code&gt; permission and works fine without it. It was a textbook case: permission requested, permission unused.&lt;/p&gt;

&lt;p&gt;The policy requires the narrowest permissions necessary for your features, and the automated checks are good at spotting a gap between the manifest and the code. We dropped &lt;code&gt;tabs&lt;/code&gt; and kept &lt;code&gt;storage&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;An iteration later, we added an "insert this page" feature, and that one needed &lt;code&gt;activeTab&lt;/code&gt; — the narrow version of the permission we'd been rejected for. It's granted when the user clicks, applies only to the active tab, and the store has no problem with it. The lesson was simple: your manifest should contain exactly what your code actually calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Firefox: declare your data, get approved immediately
&lt;/h2&gt;

&lt;p&gt;There was no rejection here, just a validator error for a missing property. Since November 3, 2025, every new extension has had to declare its data collection in the manifest through &lt;code&gt;browser_specific_settings.gecko.data_collection_permissions&lt;/code&gt;. If you collect nothing, you still have to say so explicitly with &lt;code&gt;none&lt;/code&gt;. Without that declaration, the build doesn't get signed.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;"browser_specific_settings": {&lt;br&gt;
"gecko": {&lt;br&gt;
"data_collection_permissions": {&lt;br&gt;
"required": ["authenticationInfo"]&lt;br&gt;
}&lt;br&gt;
}&lt;br&gt;
}&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;After that came automated screening and almost immediate approval. Firefox was easily the fastest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Edge: still waiting
&lt;/h2&gt;

&lt;p&gt;Technically, it's the same Chromium and the same extension, but the differences start before you get anywhere near the code. The developer program won't accept a work or school account, so we had to register a personal Microsoft account in an incognito window to stop the browser from helpfully supplying the corporate one.&lt;/p&gt;

&lt;p&gt;Then the validator said "manifest missing." The problem turned out to be the archive: &lt;code&gt;manifest.json&lt;/code&gt; has to sit at the root of the ZIP rather than inside a nested folder. We repacked it flat with &lt;code&gt;zip -j&lt;/code&gt;, and it went through.&lt;/p&gt;

&lt;p&gt;Then we waited.&lt;/p&gt;

&lt;p&gt;We got published, uploaded an update, and now we're waiting again. At least the current version stays available while the update is under review. Users keep running what's already live.&lt;/p&gt;

&lt;h2&gt;
  
  
  The HTTP 400 that only worked for me
&lt;/h2&gt;

&lt;p&gt;The team went to test the extension, and every one of them hit HTTP 400 on auto-connect. It worked fine for me.&lt;/p&gt;

&lt;p&gt;First, we ruled out authentication. A broken or empty token returns 401, and we were getting 400. The token was being accepted; something else in the request was being rejected.&lt;/p&gt;

&lt;p&gt;Then came a false lead. While digging through the web app bundle, we found that it sends both an access token and a refresh token, while the extension was only sending the first. It looked like the answer. We fixed it. It didn't help.&lt;/p&gt;

&lt;p&gt;Then I realized why it only worked for me. The code had a guard along the lines of &lt;code&gt;if (apiKey) return&lt;/code&gt;. My key was already sitting in the cache, so the mint request never fired. I hadn't touched the branch that was failing for everyone else in months.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;{ "error": "Maximum of 10 active API keys allowed" }&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The extension minted a fresh key on every connect. Over a week of testing, people had accumulated ten keys and hit the ceiling.&lt;/p&gt;

&lt;p&gt;That cost us most of a day and taught us two things. Never diagnose a 400 without reading the response body — the status code tells you the request was rejected, and only the server knows why, but it will usually tell you if you ask. And "works on my machine" can simply mean you have a cached result while the broken path only runs on a clean install.&lt;/p&gt;

&lt;p&gt;A few other things came up along the way. Store review doesn't test your integration: the reviewer has no account in your product, so functional bugs sail through, because what's being checked is policy, permissions, and the listing — a green status only means you didn't break the store's rules. Screenshots have to show your real interface, since invented marketing graphics get spotted. Trader verification is its own saga, with Organization type, a D-U-N-S number, and a name and address that have to match the registry character for character or you get "no match." And Edge has its own asset dimensions and its own promotional tile, which can't include Chrome branding.&lt;/p&gt;

&lt;h2&gt;
  
  
  So was it worth it?
&lt;/h2&gt;

&lt;p&gt;The extension is tiny, but it solves a problem I have every day, which is probably the best reason to build anything. If your product lives in a browser, an extension like this takes about a week to put together. Most of that time goes into working out what each store wants, and all three want different things.&lt;/p&gt;

&lt;p&gt;Our extension is two days old. Now we'll see whether anyone else finds it as useful as we do.&lt;/p&gt;

&lt;p&gt;We're digging further now, toward an add-on that would let you post straight from the document or deck you're working in. That's a different marketplace with different rules, so I suspect there'll be a part two.&lt;/p&gt;

&lt;p&gt;Here's the link if you want to try it: &lt;a href="https://chromewebstore.google.com/detail/publora-%E2%80%94-schedule-social/pkneoakicplhpdlhchgobccmeelhboai" rel="noopener noreferrer"&gt;Publora — Schedule Social Posts&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Have you shipped anything to the extension stores? What did they reject it for?&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>discuss</category>
      <category>javascript</category>
    </item>
    <item>
      <title>I Trained an AI to Sound Like Me. Then Spent Three Rounds Undoing It.</title>
      <dc:creator>Eugeniya Ivanova</dc:creator>
      <pubDate>Mon, 20 Jul 2026 08:24:54 +0000</pubDate>
      <link>https://dev.to/eugeniya_ivanova_4a58eadc/i-trained-an-ai-to-sound-like-me-then-spent-three-rounds-undoing-it-3k96</link>
      <guid>https://dev.to/eugeniya_ivanova_4a58eadc/i-trained-an-ai-to-sound-like-me-then-spent-three-rounds-undoing-it-3k96</guid>
      <description>&lt;p&gt;Let's get the disclosure out of the way: I wrote this with AI. This exact post went through the three rounds in the title, so if it reads clean, that's the point. If it doesn't, that's the more interesting comment.&lt;/p&gt;

&lt;p&gt;It started some paragraphs, cleaned up others, and helped turn a pile of dictated notes into something readable. If you find a sentence that sounds suspiciously polished, tell me where. I'm curious which ones I missed.&lt;/p&gt;

&lt;p&gt;I don't really think of an article as a finished object anymore. I write one version, then it has to travel. LinkedIn needs a different opening. X needs half the words. A newsletter can keep the context that a social post has to lose. Sometimes the same idea ends up in five places, each with its own limits and conventions.&lt;/p&gt;

&lt;p&gt;This is where AI saves me a lot of time. It can cut, rearrange, reformat, and produce ten versions without getting bored. The original writing is another matter. I still don't trust it to do that alone.&lt;/p&gt;

&lt;p&gt;Writing is fashionable to declare dead right now. Why bother learning when AI can generate a post in seconds? It reminds me of the way people talked about SEO around 2016. SEO didn't disappear. It changed, picked up a few new abbreviations, and carried on taking up everyone's time.&lt;/p&gt;

&lt;p&gt;I suspect writing will do the same.&lt;/p&gt;

&lt;p&gt;AI-generated text is already recognizable, although it isn't always easy to explain why. Certain phrases give it away immediately. &lt;strong&gt;"Here's what I found."&lt;/strong&gt; &lt;strong&gt;"Not generic. Not robotic. You."&lt;/strong&gt; &lt;strong&gt;"Honestly,"&lt;/strong&gt; placed at the start of a sentence as though everything before it had been dishonest.&lt;/p&gt;

&lt;p&gt;The model reaches for these phrases when it needs a bridge but has nothing specific to add. Once you've edited enough generated text, you start spotting them before you've properly read the sentence.&lt;/p&gt;

&lt;p&gt;The structure is often more revealing than the vocabulary. A text begins in ordinary paragraphs, then gradually turns vertical.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Like this.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One sentence per line.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Each thought given the same weight as the last.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Looks clean, doesn't it. Reads with a certain rhythm.&lt;/p&gt;

&lt;p&gt;Then the paragraph snaps back to full width and you notice you'd stopped following an argument and started being walked down a list, with every thought given the same space and emphasis whether it earned it or not. It reads especially well on a phone, which is part of the problem.&lt;/p&gt;

&lt;p&gt;You can tell the model to write in paragraphs, vary the sentence length, and avoid staccato. It will follow the instruction for a while, then drift back. That's the style it has seen rewarded across a huge amount of online writing.&lt;/p&gt;

&lt;p&gt;The current panic about AI writing has also made careful human writing look suspicious.&lt;/p&gt;

&lt;p&gt;I once wrote a comment entirely by hand. It had proper paragraphs, a clear structure, and punctuation exactly where I wanted it. An automated moderator removed it almost immediately.&lt;/p&gt;

&lt;p&gt;No person read it and decided it was bad. A system decided, from the pattern alone, that it looked generated.&lt;/p&gt;

&lt;p&gt;The comment was too orderly. The paragraphs were balanced, the argument moved cleanly from one point to the next, and apparently a real person was not expected to write that smoothly. I had been classified as a robot for taking the time to edit.&lt;/p&gt;

&lt;p&gt;Since then, I've been skeptical whenever someone presents a list of definitive "AI tells." Sometimes those patterns do come from AI. Sometimes they come from people who know how to construct a paragraph.&lt;/p&gt;

&lt;p&gt;The em dash is the most ridiculous example. Apparently it belongs to language models now. This thing, the one I just used, is supposed to be a confession.&lt;/p&gt;

&lt;p&gt;I've been typing it for fourteen years. My fingers learned Shift-Option-hyphen long before I knew what a large language model was. I'm not going to retrain them because automated detectors have developed an anxiety about punctuation.&lt;/p&gt;

&lt;p&gt;The dash is a poor test anyway. AI usually reveals itself at a larger scale: a rhythm that never changes, groups of three arranged a little too neatly, repeated "not X, but Y" constructions, and sentences that sound complete without containing much of anything. A single punctuation mark tells you very little.&lt;/p&gt;

&lt;p&gt;So what do I actually do when AI has touched a draft?&lt;/p&gt;

&lt;p&gt;First, I read the whole thing aloud. My eyes will accept a sentence because it looks polished. My ears are less forgiving. They catch the places where the language is doing more work than the idea.&lt;/p&gt;

&lt;p&gt;Then I check the meaning sentence by sentence. What is this claiming? Where did that claim come from? Is it saying something concrete, or is it only moving me toward the next paragraph?&lt;/p&gt;

&lt;p&gt;Unsourced facts either get a source or disappear. Tenses and numbers get checked separately because models mix both up with remarkable confidence.&lt;/p&gt;

&lt;p&gt;After that, I give the draft to a second AI with a simple instruction: find and remove anything that sounds generated. It usually spots phrases I no longer notice because I've been staring at the text for too long.&lt;/p&gt;

&lt;p&gt;Then I edit its edit.&lt;/p&gt;

&lt;p&gt;In removing clichés, the second pass often removes meaning, tone, or some awkward detail that made the paragraph sound like me. It tends to solve messiness by making everything neutral. I compare the versions, put back what mattered, and rewrite the rest by hand.&lt;/p&gt;

&lt;p&gt;That final pass is usually where the text stops feeling generated.&lt;/p&gt;

&lt;p&gt;I have a personal list of things that make me pause: "delve," "seamless," "game-changer," empty bridges such as "here's what I found," too many mirrored contrasts, and staccato that runs through a whole piece. None of these is automatically forbidden. People use them too. But each one makes me check whether the sentence exists because I meant it or because the model needed something plausible in that spot.&lt;/p&gt;

&lt;p&gt;The process isn't as new as it sounds. When I was learning to write, I did roughly the same thing with human editors.&lt;/p&gt;

&lt;p&gt;Several of my friends are editors, and they were not gentle. They crossed out clever sentences, asked where facts came from, and pointed to the exact line where I had stopped saying anything. They argued with me when I defended a phrase only because I liked how it sounded.&lt;/p&gt;

&lt;p&gt;Back then, I was training myself through repeated feedback. Now I'm training a model on top of that. The method has barely changed: show examples, explain what failed, revise, and don't accept the first pass just because it's grammatically correct.&lt;/p&gt;

&lt;p&gt;I don't ask AI to "write in an interesting voice." That instruction usually produces a polished average of everything online. Instead, I give it concrete examples of writing whose rhythm I like and explain what I want it to notice: how long the paragraphs run, where the author gets specific, how jokes are placed, and which ideas are allowed to stay slightly rough.&lt;/p&gt;

&lt;p&gt;Even then, the best results rarely begin with a blank page.&lt;/p&gt;

&lt;p&gt;AI works much better when I give it a raw draft: dictated, repetitive, badly ordered, but mine. It can untangle the structure and cut the repetition without having to invent the material. Ask it to start from nothing and you generally get something competent that you'd never have chosen to say yourself.&lt;/p&gt;

&lt;p&gt;The human still has to bring the observation, the story, and the reason for writing at all. A model is good at combing through that material. Give it a blank page and ask it to find the point, and that's where it starts to fall apart.&lt;/p&gt;

&lt;p&gt;Once the article is finished, the balance changes. It still needs to become a LinkedIn post, a shorter version for X, an email, maybe a few replies. That work is repetitive, rule-based, and easy to verify. I hand it to AI without regret.&lt;/p&gt;

&lt;p&gt;Publishing is repetitive too. I handle that through Publora now, which I'm part of, so weigh the mention accordingly. But the distinction holds no matter the tool: writing an idea and reproducing it across formats are two different jobs, and only one of them is the reason I started.&lt;/p&gt;

&lt;p&gt;So no, I don't think writing is dead. I think the work around it is changing. There's more generated text to edit, more polished emptiness to catch, and more reason to know what your own voice sounds like before you ask a model to imitate it.&lt;/p&gt;

&lt;p&gt;It's slower than typing "write me a post" and publishing what comes back. It also gives me something I can still recognize as mine.&lt;/p&gt;

&lt;p&gt;AI helped write this article. Which sentence gave it away?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>writing</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Why Your PNG Works on Threads and Dies on Instagram</title>
      <dc:creator>Eugeniya Ivanova</dc:creator>
      <pubDate>Wed, 15 Jul 2026 12:45:05 +0000</pubDate>
      <link>https://dev.to/eugeniya_ivanova_4a58eadc/why-your-png-works-on-threads-and-dies-on-instagram-5co6</link>
      <guid>https://dev.to/eugeniya_ivanova_4a58eadc/why-your-png-works-on-threads-and-dies-on-instagram-5co6</guid>
      <description>&lt;p&gt;You have one image to post to Instagram, Threads, and Bluesky. Two of those belong to the same company. It sounds like it should be easy.&lt;/p&gt;

&lt;p&gt;Start with the file format. Instagram's docs say only JPEG is supported. Threads officially supports both JPEG and PNG. Same company, different rules — the exact same PNG uploads to Threads and gets rejected by Instagram.&lt;/p&gt;

&lt;p&gt;Carousels are another one. Instagram allows up to 10 items; Threads allows up to 20, with a minimum of 2. Again: same company, same feature, different limits.&lt;/p&gt;

&lt;p&gt;Then there are file sizes. Threads accepts images up to 8 MB. Bluesky's limit is exactly 1,000,000 bytes — not "about a megabyte," their own sample code rejects anything larger than &lt;code&gt;1000000&lt;/code&gt;. A 2.5 MB photo works on two platforms and fails on the third.&lt;/p&gt;

&lt;p&gt;The bigger difference, though, isn't the limits. It's how the platforms expect images to reach them.&lt;/p&gt;

&lt;p&gt;Meta doesn't want your file. It wants a URL.&lt;/p&gt;

&lt;p&gt;You put the image somewhere publicly accessible, send &lt;code&gt;image_url&lt;/code&gt;, and, as the docs put it, "we will cURL your image using the URL provided." So before you can publish an image to Instagram, you need somewhere to host it.&lt;/p&gt;

&lt;p&gt;Bluesky works the other way around. You upload the bytes with &lt;code&gt;uploadBlob&lt;/code&gt;, get back a blob reference, then include that reference in your post. The post never contains the image itself, it only points to it. Same idea, completely different model.&lt;/p&gt;

&lt;p&gt;Meta also splits publishing into two steps: first you create a media container, then you publish it. The container expires after 24 hours, and if you miss the window you start over. Instagram recommends polling its status once a minute for up to five minutes. Threads suggests waiting about thirty seconds before publishing.&lt;/p&gt;

&lt;p&gt;None of that shows up in the "post in three lines of code" examples. Your scheduler still has to deal with it.&lt;/p&gt;

&lt;p&gt;Reels add another surprise. The upload endpoint isn't &lt;code&gt;graph.facebook.com&lt;/code&gt;, it's &lt;code&gt;rupload.facebook.com&lt;/code&gt;. Different hostname, same feature.&lt;/p&gt;

&lt;p&gt;Bluesky has its own rules. Every image needs alt text, and if you don't have a description, you still send an empty string. It feels pedantic, but that's the API. The docs also recommend stripping EXIF data before upload — the server may enforce it later, but today it's still your responsibility.&lt;/p&gt;

&lt;p&gt;One detail made me smile. Threads counts emojis against the 500-character limit by their UTF-8 byte length, so one 👋 counts as four. I'd already run into byte offsets while writing about Bluesky facets and assumed it was one of those open-protocol quirks. Apparently not.&lt;/p&gt;

&lt;p&gt;Bytes come for everyone.&lt;/p&gt;

&lt;p&gt;While we're talking about documentation, Instagram's own content publishing page says API-published posts are limited to 100 per rolling 24 hours. Further down the same page, in the carousel section, it says 50. Both numbers are in the official documentation.&lt;/p&gt;

&lt;p&gt;At Publora we eventually ended up exposing one POST with &lt;code&gt;content&lt;/code&gt;, &lt;code&gt;platforms&lt;/code&gt;, and &lt;code&gt;scheduledTime&lt;/code&gt;. For media you either provide &lt;code&gt;mediaUrls&lt;/code&gt; — up to ten public HTTPS URLs in the same request, fetched server-side much like Meta does — or, if you're uploading local files, create a draft first, upload to a pre-signed S3 URL, then schedule.&lt;/p&gt;

&lt;p&gt;If I weren't working on Publora, my advice would still be the same. If you're integrating with one network, use its native API: they're well documented, and you'll understand how the platform actually works.&lt;/p&gt;

&lt;p&gt;Once you're supporting multiple platforms, the hard part stops being the HTTP requests. It's everything each platform quietly means when it says "upload an image."&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;p&gt;These are the primary sources for everything above.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://developers.facebook.com/docs/instagram-platform/content-publishing/" rel="noopener noreferrer"&gt;Instagram Content Publishing&lt;/a&gt; — containers, statuses, rate limits&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developers.facebook.com/docs/threads/posts" rel="noopener noreferrer"&gt;Threads Posts&lt;/a&gt; — media specifications, carousels, the emoji-byte note&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.bsky.app/blog/create-post" rel="noopener noreferrer"&gt;Posting via the Bluesky API&lt;/a&gt; — blobs, facets, alt text&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Every limit and behavior above comes from the platforms' own documentation. Where the docs disagree with themselves, I've pointed it out.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>webdev</category>
      <category>programming</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I called Bluesky simple. Time for the asterisks.</title>
      <dc:creator>Eugeniya Ivanova</dc:creator>
      <pubDate>Fri, 10 Jul 2026 09:59:27 +0000</pubDate>
      <link>https://dev.to/eugeniya_ivanova_4a58eadc/i-called-bluesky-simple-time-for-the-asterisks-135g</link>
      <guid>https://dev.to/eugeniya_ivanova_4a58eadc/i-called-bluesky-simple-time-for-the-asterisks-135g</guid>
      <description>&lt;p&gt;In my last post I called posting to Bluesky "refreshingly straightforward." I still think that's true. I just stopped at "Hello, world."&lt;/p&gt;

&lt;p&gt;Getting started really is easy. One request creates a session with your handle and app password. Another writes a record into &lt;code&gt;app.bsky.feed.post&lt;/code&gt;. Two API calls later, your post is live.&lt;/p&gt;

&lt;p&gt;Then you add a link.&lt;/p&gt;

&lt;p&gt;The URL is sitting right there in the text, but it isn't clickable. Bluesky doesn't automatically treat URLs as links. Instead, you describe where the link appears using facets—metadata that points to the exact byte range occupied by the URL.&lt;/p&gt;

&lt;p&gt;And "byte" is the word that ruins your afternoon.&lt;/p&gt;

&lt;p&gt;If your post is plain ASCII, character positions and byte positions happen to match. Add Unicode, and they no longer do. Put one 👋 at the beginning of the post and every offset after it shifts. The text still looks right, but your facet now points somewhere else.&lt;/p&gt;

&lt;p&gt;If you've ever wondered why the official SDK ships a &lt;code&gt;RichText&lt;/code&gt; helper, this is why. It detects links, mentions, and hashtags, then computes the byte offsets correctly so you don't have to.&lt;/p&gt;

&lt;p&gt;Tokens have their own catch.&lt;/p&gt;

&lt;p&gt;Bluesky gives you two: a short-lived access token and a longer-lived refresh token. Log in and post immediately, and the access token is usually enough. Schedule the post for later, and it may have expired by the time the job runs. At that point, refreshing it becomes part of the normal publishing flow.&lt;/p&gt;

&lt;p&gt;Images are another step of their own. Upload each image as a blob, then reference it in the post record. You can attach up to four images. The post itself is limited to 300 characters, so if you're over the limit, deciding what to cut is your responsibility.&lt;/p&gt;

&lt;p&gt;None of these pieces is especially difficult. The friction comes from having to deal with all of them yourself. "Send a post" quietly becomes "calculate byte offsets, refresh tokens, upload blobs, validate limits."&lt;/p&gt;

&lt;p&gt;That's perfectly reasonable if you're building directly on AT Protocol.&lt;/p&gt;

&lt;p&gt;At Publora, we hide those details behind a single request: content, platforms, and an optional scheduled time. Facets for links, mentions, and hashtags are generated automatically, tokens are refreshed when needed, images are uploaded, and posts over 300 characters are trimmed to fit. It's the same Bluesky API underneath—we just take care of the repetitive parts.&lt;/p&gt;

&lt;p&gt;I'm obviously biased, so here's the recommendation I'd make anyway: if you're building specifically for Bluesky, use the official SDK. It's well designed and saves you from implementing the fiddly bits yourself. But once Bluesky becomes one destination among several, another layer starts to make a lot of sense.&lt;/p&gt;

&lt;p&gt;The first two API calls really are refreshingly simple.&lt;/p&gt;

&lt;p&gt;Everything after them is the actual integration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;p&gt;For the full details—facets, blobs, replies, threads, embeds, and more—the official guide is the place to start:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.bsky.app/blog/create-post" rel="noopener noreferrer"&gt;Posting via the Bluesky API&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And if you'd rather follow someone building a full CLI from scratch:&lt;/p&gt;


&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/retrorom/mastering-the-at-protocol-building-a-full-featured-bluesky-cli-from-scratch-23hj" class="crayons-story__hidden-navigation-link"&gt;Mastering the AT Protocol: Building a Full-Featured Bluesky CLI from Scratch&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/retrorom" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3787291%2Fabb54b89-011f-41bb-8fcd-42c2a3338e8c.PNG" alt="retrorom profile" class="crayons-avatar__image" width="209" height="203"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/retrorom" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Retrorom
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Retrorom
                
              
              &lt;div id="story-author-preview-content-3291742" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/retrorom" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3787291%2Fabb54b89-011f-41bb-8fcd-42c2a3338e8c.PNG" class="crayons-avatar__image" alt="" width="209" height="203"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Retrorom&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/retrorom/mastering-the-at-protocol-building-a-full-featured-bluesky-cli-from-scratch-23hj" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Feb 27&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/retrorom/mastering-the-at-protocol-building-a-full-featured-bluesky-cli-from-scratch-23hj" id="article-link-3291742"&gt;
          Mastering the AT Protocol: Building a Full-Featured Bluesky CLI from Scratch
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/automation"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;automation&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/bluesky"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;bluesky&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/cli"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;cli&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/atprotocol"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;atprotocol&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/retrorom/mastering-the-at-protocol-building-a-full-featured-bluesky-cli-from-scratch-23hj" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="" height=""&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/raised-hands-74b2099fd66a39f2d7eed9305ee0f4553df0eb7b4f11b01b6b1b499973048fe5.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;5&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/retrorom/mastering-the-at-protocol-building-a-full-featured-bluesky-cli-from-scratch-23hj#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            8 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


&lt;p&gt;&lt;em&gt;Drafted with AI, then reviewed against the Bluesky documentation and Publora's own implementation guide before publishing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>bluesky</category>
      <category>atprotocol</category>
      <category>api</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Why Every AI Agent Eventually Fights Social Media APIs</title>
      <dc:creator>Eugeniya Ivanova</dc:creator>
      <pubDate>Tue, 07 Jul 2026 13:29:35 +0000</pubDate>
      <link>https://dev.to/eugeniya_ivanova_4a58eadc/why-every-ai-agent-eventually-fights-social-media-apis-35e0</link>
      <guid>https://dev.to/eugeniya_ivanova_4a58eadc/why-every-ai-agent-eventually-fights-social-media-apis-35e0</guid>
      <description>&lt;p&gt;AI agents have gotten weirdly good at writing.&lt;br&gt;
You can hand one a vague prompt and get back something publishable. The trouble starts one step later, when you ask it to actually press "post."&lt;br&gt;
"Post this to LinkedIn" sounds like one API call. In practice it's multiple OAuth flows, a few different media upload pipelines, review-gated permissions, token refresh logic, rate limits, and a scheduler for the moment you decide you'd rather not publish at 3am.&lt;br&gt;
The model isn't the hard part anymore.&lt;br&gt;
The integrations are.&lt;br&gt;
If you're building agents that publish, here's what that landscape actually looks like.&lt;/p&gt;
&lt;h2&gt;
  
  
  One network? Native is great.
&lt;/h2&gt;

&lt;p&gt;Let's start with the easy case. Posting to Bluesky via the AT Protocol is refreshingly straightforward.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create a session&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://bsky.social/xrpc/com.atproto.server.createSession &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"identifier":"you.bsky.social","password":"'&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BLUESKY_APP_PASSWORD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s1"&gt;'"}'&lt;/span&gt;

&lt;span class="c"&gt;# Create a post&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://bsky.social/xrpc/com.atproto.repo.createRecord &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$ACCESS_JWT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "repo": "&amp;lt;your-did&amp;gt;",
    "collection": "app.bsky.feed.post",
    "record": {
      "text": "Hello Bluesky 👋",
      "createdAt": "2026-07-10T09:00:00Z"
    }
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Two requests. That's the whole thing.&lt;br&gt;
Now imagine doing that for LinkedIn, X, Instagram, Facebook, Threads, and Mastodon—none of which were designed together, or, from the look of it, designed to be aware the others exist.&lt;br&gt;
There isn't really a "social media API." There are half a dozen unrelated APIs that happen to share a hobby.&lt;/p&gt;
&lt;h2&gt;
  
  
  Every platform has its own rules
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Authentication&lt;/th&gt;
&lt;th&gt;Biggest friction&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;LinkedIn&lt;/td&gt;
&lt;td&gt;OAuth 2.0&lt;/td&gt;
&lt;td&gt;App review for advanced publishing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;OAuth 2.0&lt;/td&gt;
&lt;td&gt;Pay-per-use since Feb 2026 — ~$0.015/post, &lt;strong&gt;$0.20 if it has a link&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Instagram / Facebook / Threads&lt;/td&gt;
&lt;td&gt;Meta Graph API&lt;/td&gt;
&lt;td&gt;Business account + App Review&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bluesky&lt;/td&gt;
&lt;td&gt;App password or OAuth&lt;/td&gt;
&lt;td&gt;Almost no friction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mastodon&lt;/td&gt;
&lt;td&gt;Token per instance&lt;/td&gt;
&lt;td&gt;Every instance is effectively its own API&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The pattern shows up fast. Open networks like Bluesky and Mastodon are easy to build against; the big commercial platforms want app reviews, business verification, paid access, or all three.&lt;br&gt;
X earns its own line here. Writing isn't just paid—it's paid per link. A plain post runs about $0.015, but a post with a URL costs $0.20. Which is a delightful surprise for an agent whose entire job is sharing links. You built automation to save time, and now it's quietly running up a tab, twenty cents at a time.&lt;/p&gt;
&lt;h2&gt;
  
  
  The integration work sneaks up on you
&lt;/h2&gt;

&lt;p&gt;Generating content is easy. Keeping the integrations alive is the actual job.&lt;br&gt;
Every platform authenticates differently, uploads media differently, scopes differently, fails differently, and expires its tokens on its own private schedule. Scheduling, half the time, isn't supported at all.&lt;br&gt;
At some point your AI project quietly turns into an OAuth project.&lt;/p&gt;
&lt;h2&gt;
  
  
  Where MCP fits
&lt;/h2&gt;

&lt;p&gt;One way out is to stop teaching the agent about every platform individually, and instead expose publishing as a single tool.&lt;/p&gt;

&lt;p&gt;With the Model Context Protocol (MCP), an agent discovers available tools with tools/list and calls them with tools/call. Under the hood it's plain JSON-RPC.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"jsonrpc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tools/call"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"params"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"create_post"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hello from an agent 👋"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"platforms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"linkedin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"bluesky"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"scheduledTime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-07-10T09:00:00Z"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The interesting part isn't MCP itself. It's the abstraction.&lt;/p&gt;

&lt;p&gt;The agent calls one tool. Something behind that tool deals with OAuth, retries, uploads, scheduling, and whatever mood each platform is in today.&lt;/p&gt;

&lt;p&gt;It's a pattern engineers already know by heart: hide the provider-specific mess behind a stable interface, and try not to look at it too often.&lt;/p&gt;

&lt;p&gt;I'm skipping the protocol tour on purpose—teaching MCP from scratch would pull this post somewhere it doesn't need to go. If you want the proper walkthrough of tools/list, tools/call, and the JSON-RPC plumbing underneath, other people have already done it well:&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/_d7eb1c1703182e3ce1782/mcp-server-tutorial-build-your-first-model-context-protocol-server-step-by-step-1ih" class="crayons-story__hidden-navigation-link"&gt;MCP Server Tutorial: Build Your First Model Context Protocol Server Step by Step&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/_d7eb1c1703182e3ce1782" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3834003%2Fe97eead8-8240-4d24-9909-25d6c8d411ce.png" alt="_d7eb1c1703182e3ce1782 profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/_d7eb1c1703182e3ce1782" class="crayons-story__secondary fw-medium m:hidden"&gt;
              楊東霖
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                楊東霖
                
              
              &lt;div id="story-author-preview-content-3374386" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/_d7eb1c1703182e3ce1782" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3834003%2Fe97eead8-8240-4d24-9909-25d6c8d411ce.png" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;楊東霖&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/_d7eb1c1703182e3ce1782/mcp-server-tutorial-build-your-first-model-context-protocol-server-step-by-step-1ih" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Mar 20&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/_d7eb1c1703182e3ce1782/mcp-server-tutorial-build-your-first-model-context-protocol-server-step-by-step-1ih" id="article-link-3374386"&gt;
          MCP Server Tutorial: Build Your First Model Context Protocol Server Step by Step
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/webdev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;webdev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/programming"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;programming&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/tools"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;tools&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/productivity"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;productivity&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
            &lt;a href="https://dev.to/_d7eb1c1703182e3ce1782/mcp-server-tutorial-build-your-first-model-context-protocol-server-step-by-step-1ih#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            18 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;/div&gt;
&lt;br&gt;
&lt;br&gt;&lt;br&gt;
…and the official MCP docs are the canonical reference. No point in me repeating what they cover better.

&lt;h2&gt;
  
  
  Connecting an MCP server
&lt;/h2&gt;

&lt;p&gt;A hosted MCP server is usually just an endpoint and a key. Here's how I connect the one we built at Publora:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add publora &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--transport&lt;/span&gt; http https://mcp.publora.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$PUBLORA_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Publora, your own server, someone else's implementation—it doesn't matter for the point. The architecture stays the same: your agent calls one tool instead of maintaining a relationship with five different APIs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things that cost far more time than expected
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Scheduling&lt;/strong&gt;&lt;br&gt;
Most APIs publish immediately. If you want scheduled posts, you're building a queue, retries, and a scheduler yourself—congratulations, you now maintain infrastructure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Media uploads&lt;/strong&gt;&lt;br&gt;
Uploading media is almost never one request. Usually it's three:&lt;/p&gt;

&lt;p&gt;Ask for an upload target.&lt;br&gt;
Upload the file.&lt;br&gt;
Reference the returned media ID.&lt;/p&gt;

&lt;p&gt;And every platform has its own opinion about steps one through three.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Partial failures&lt;/strong&gt;&lt;br&gt;
"Publish everywhere" is never atomic. Three platforms succeed, one rate-limits you, and one rejects your image for reasons it declines to specify. It's a group project, and one member has gone quiet.&lt;br&gt;
So now you need retry policies and a way to tell the user what actually happened without lying to them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Formatting&lt;/strong&gt;&lt;br&gt;
Character limits, mentions, hashtags, markdown, media rendering—every platform supports roughly the same ideas and implements none of them the same way. The same post rarely looks identical in two places, and occasionally it shows up in one of them looking like a ransom note.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Token management&lt;/strong&gt;&lt;br&gt;
OAuth refresh tokens, app passwords, per-instance tokens—each with its own expiration rules and its own refresh flow. Authentication itself isn't the hard part. Supporting all of it at the same time, forever, is what quietly eats your week.&lt;/p&gt;

&lt;h2&gt;
  
  
  So... should you build it yourself?
&lt;/h2&gt;

&lt;p&gt;If you're targeting one open network like Bluesky—absolutely, go native. The APIs are clean and you'll come out understanding the protocol properly.&lt;br&gt;
Once you add several networks, plus scheduling, media, and retries, the math flips. Not because any single request is difficult, but because maintaining six integrations is not the reason any of us got into this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;AI didn't make social media publishing easier. It just made it impossible to ignore how fragmented the whole thing already was.&lt;br&gt;
The hard part isn't generating text anymore. It's everything wrapped around it: auth, permissions, uploads, scheduling, retries, and a long tail of platform-specific edge cases.&lt;br&gt;
Whether you handle that with direct integrations or hide it behind MCP is an architectural call. Knowing where the complexity actually lives is the part worth keeping.&lt;/p&gt;




&lt;p&gt;I work on Publora, so that's the MCP server I know best—and it's open source (MIT) if you want to poke at it. But even if you never touch it, try wiring something up to Bluesky's AT Protocol, or stand up a tiny MCP server of your own. It's one of those weekend projects that makes all the trade-offs obvious by Sunday.&lt;br&gt;
What are you using to connect your agents to the outside world these days?&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Drafted with AI, then read start to finish by an actual human—which, given the topic, felt like the least I could do.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>api</category>
      <category>mcp</category>
    </item>
  </channel>
</rss>
