<?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 got our API into the Postman network in one evening — and didn't hand-write a single request</title>
      <dc:creator>Eugeniya Ivanova</dc:creator>
      <pubDate>Tue, 18 Aug 2026 07:30:59 +0000</pubDate>
      <link>https://dev.to/eugeniya_ivanova_4a58eadc/i-got-our-api-into-the-postman-network-in-one-evening-and-didnt-hand-write-a-single-request-2an6</link>
      <guid>https://dev.to/eugeniya_ivanova_4a58eadc/i-got-our-api-into-the-postman-network-in-one-evening-and-didnt-hand-write-a-single-request-2an6</guid>
      <description>&lt;p&gt;About a quarter of our users access the API with a key. The main barrier isn't code, it's the first half hour: open the docs, figure out which endpoints you need, put together the first request, find where the key goes. The Postman network skips most of that. You get ready-made collections you can fork into your workspace and run with your own key. We're in there now, next to Notion, Stripe, and Twilio — and getting listed took one evening. Here's how I did it, and why I decided not to build the collection by hand.&lt;/p&gt;

&lt;p&gt;First, a pleasant surprise. Getting listed anywhere usually means sitting in someone's review queue — Chrome, Edge, and LinkedIn have all made us wait. Postman doesn't work that way: it's self-serve, publishing is free, and there's no application or moderation. So the whole thing really did fit into an evening.&lt;/p&gt;

&lt;p&gt;You can build a collection for the network by hand: create the requests, type in the paths, fields, and headers. Plenty of people do. The problem is that a hand-built collection is a snapshot of your API on the day you made it. Add an endpoint or rename a field a month later, and now the collection is out of date.&lt;/p&gt;

&lt;p&gt;So I didn't write ours. I imported our OpenAPI spec. Ours is public and has 29 paths:&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="err"&gt;https://docs.publora.com/openapi.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The import is one call to the Postman API:&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;--location&lt;/span&gt; &lt;span class="s1"&gt;'https://api.getpostman.com/import/openapi'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s1"&gt;'X-Api-Key: PMAK-YOUR-KEY'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s1"&gt;'{
    "type": "json",
    "options": { "folderStrategy": "Path" },
    "input": &amp;lt;contents of openapi.json&amp;gt;
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That means the paths, fields, and types come straight from the spec and match the actual API. When the API changes, you can regenerate the collection from the latest spec instead of manually syncing everything again.&lt;/p&gt;

&lt;p&gt;There's one thing to watch for. Notice &lt;code&gt;folderStrategy: "Path"&lt;/code&gt; — Postman organizes the imported requests by URL path. That's technically correct, but not particularly nice to browse: you end up with a wall of paths instead of useful groups. So importing was only half the job. I reorganized the collection through &lt;code&gt;PUT /collections/{uid}&lt;/code&gt; into something a person can actually navigate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Connections   3   connected accounts, platform limits, connection test
Posts         6   create, schedule, get, update, delete, logs
Media         5   three-step file upload, Reels cover, YouTube thumbnail
LinkedIn      9   analytics, followers, comments, reactions, repost
Webhooks      5   notifications instead of polling
Workspace     6   managing client accounts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A note about keys, since this is a public catalog rather than my private workspace. Authentication came from the spec automatically: the &lt;code&gt;x-publora-key&lt;/code&gt; header gets its value from an &lt;code&gt;apiKey&lt;/code&gt; variable, and that variable is empty. Anyone who forks the collection adds their own key. I checked the published version to make sure mine wasn't there. And if you're doing this yourself, don't put your Postman key (&lt;code&gt;PMAK-…&lt;/code&gt;) in the collection either. You need it for the import call, not for the public collection.&lt;/p&gt;

&lt;p&gt;Instead of writing a long introduction, I added a short "first post in 60 seconds" and a "where everyone trips" section. That felt more useful than repeating the docs. It covers the things we see people get wrong: copy the &lt;code&gt;platformId&lt;/code&gt; exactly or the post can go to the wrong account; Instagram and TikTok don't accept text-only posts; leave out the publish time and you get a draft rather than a published post; comments are LinkedIn-only; and you can test the whole flow without publishing anything by using the &lt;code&gt;publora-playground&lt;/code&gt; target.&lt;/p&gt;

&lt;p&gt;Two things tripped me up, so they're worth mentioning.&lt;/p&gt;

&lt;p&gt;Workspace visibility can't be changed through the API. You can do almost everything else in code, but switching the workspace from Internal to Public has to be done manually in settings — in the same place where you rename the team. Not a big deal once you know, but I spent some time looking for an API setting that doesn't exist.&lt;/p&gt;

&lt;p&gt;And one more general lesson: trust the machine-readable spec over the overview page. Overview docs can lag behind the API. The spec is much more likely to reflect what's actually implemented. If I'm wiring up an integration against someone else's API, that's where I'd look first.&lt;/p&gt;

&lt;p&gt;The short version: getting into the Postman network can take an evening, and the useful shortcut is not writing the collection by hand. Generate it from the spec, clean up the structure, leave the key field empty, and regenerating it after API changes becomes much easier.&lt;/p&gt;

&lt;p&gt;If you're in Postman and want to have a look, here's our collection: &lt;a href="https://www.postman.com/publora/publora-api/overview" rel="noopener noreferrer"&gt;Publora API on Postman&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For those publishing your own APIs: do you generate collections from the spec or still build them by hand? And how often do you find your overview docs have drifted from the actual spec?&lt;/p&gt;

</description>
      <category>api</category>
      <category>postman</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>We shipped a one-click 'Add to Cursor' button. The hard part wasn't the button.</title>
      <dc:creator>Eugeniya Ivanova</dc:creator>
      <pubDate>Mon, 10 Aug 2026 12:17:01 +0000</pubDate>
      <link>https://dev.to/eugeniya_ivanova_4a58eadc/we-shipped-a-one-click-add-to-cursor-button-the-hard-part-wasnt-the-button-141m</link>
      <guid>https://dev.to/eugeniya_ivanova_4a58eadc/we-shipped-a-one-click-add-to-cursor-button-the-hard-part-wasnt-the-button-141m</guid>
      <description>&lt;p&gt;My job is getting our product into people's hands. It's built for AI agents, so it has to live where agents pick up their tools — in their catalogs. Slow business: we're in the review queue at the big ones as I write this. And getting listed is only half the job. The other half is making sure connecting isn't a scavenger hunt through config files, pasted keys, and hopeful restarts.&lt;/p&gt;

&lt;p&gt;So we started with a button. One click, Publora's in Cursor. Sounds like an evening's work.&lt;/p&gt;

&lt;p&gt;It wasn't.&lt;/p&gt;

&lt;p&gt;Cursor installs MCP servers from a deeplink — a link that tells the editor to add a given server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cursor://anysphere.cursor-deeplink/mcp/install?name=Publora&amp;amp;config=&amp;lt;base64&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The config is a base64-encoded JSON holding the server address. Official format, everyone uses it, Asana and Xano included. We built the button, the link generated cleanly, the base64 decoded into exactly the right config. Everything checked out.&lt;/p&gt;

&lt;p&gt;You click it, and: connection failed. Before it ever reached authentication.&lt;/p&gt;

&lt;p&gt;My first guess was the key. It won't connect, so it must be access, right? Wrong, and that wrong guess is where the time goes.&lt;/p&gt;

&lt;p&gt;The button doesn't ask for a key, it drops in a placeholder. Cursor sends that placeholder, the server returns a perfectly correct 401, and instead of reporting a bad key, Cursor falls back to OAuth. It goes looking for OAuth settings at the standard spot, &lt;code&gt;/.well-known/oauth-authorization-server&lt;/code&gt;. Our server answered there with a custom response of its own — not the shape the spec describes. Cursor tried to parse it as OAuth metadata, found none of the fields it needed, printed "MCP OAuth provider initialized," and stopped there.&lt;/p&gt;

&lt;p&gt;Placeholder instead of a key, then a 401, then a fallback into OAuth, then a failure on our non-standard response. The key was never the problem. It's just the first thing everyone suspects.&lt;/p&gt;

&lt;p&gt;Our first fix was simple: type a real key in by hand. Cursor has a little pencil that opens &lt;code&gt;~/.cursor/mcp.json&lt;/code&gt;, you swap the placeholder for a real &lt;code&gt;sk_...&lt;/code&gt;, and it goes green. A valid key means the request never reaches OAuth discovery at all — the server just answers and hands over its tools. Fine. But "one click, now paste your key" is a click and a half, and not the button we came to build.&lt;/p&gt;

&lt;p&gt;Now, before anyone asks why we didn't do it properly from day one. A key instead of OAuth wasn't us cutting corners. It's where the whole ecosystem started: the MCP spec recommended API keys until March 2025 and only then made OAuth the baseline. The thing has been &lt;a href="https://www.descope.com/blog/post/mcp-auth-spec" rel="noopener noreferrer"&gt;rewritten three times in nine months&lt;/a&gt; — catching up to that is not an evening either. This same &lt;code&gt;.well-known&lt;/code&gt; is currently tripping up companies with rather more headcount than us: Atlassian has an &lt;a href="https://github.com/atlassian/atlassian-mcp-server/issues/205" rel="noopener noreferrer"&gt;open bug&lt;/a&gt; about OAuth-metadata validation, and Microsoft's own MCP clients &lt;a href="https://learn.microsoft.com/en-nz/answers/questions/5904511/mcp-client-ignores-authorization-servers-path-from" rel="noopener noreferrer"&gt;resolve the endpoint wrong&lt;/a&gt;. And when one study checked &lt;a href="https://www.obsidiansecurity.com/blog/when-mcp-meets-oauth-common-pitfalls-leading-to-one-click-account-takeover" rel="noopener noreferrer"&gt;660 OAuth servers, 27 of them&lt;/a&gt; — four percent — did full dynamic registration. Moving from a key to real OAuth isn't the slow kid's homework. It's the part most products just never finish.&lt;/p&gt;

&lt;p&gt;So instead of hiding the key, we fixed the place everything kept dying on. Cursor wants OAuth so badly? Give it OAuth. My engineers put proper, to-spec metadata at &lt;code&gt;/.well-known/oauth-authorization-server&lt;/code&gt; — issuer, authorize and token endpoints, client registration, PKCE, the whole set the editor kept hunting for. My contribution at this stage was reading closely and sitting next to them waiting for a miracle. It arrived.&lt;/p&gt;

&lt;p&gt;The chain runs straight now. Click the button, Cursor fetches the OAuth settings, finds real ones, pops the Publora login, you sign in, and it collects the token itself. Nothing typed by hand. The one click, as advertised.&lt;/p&gt;

&lt;p&gt;To reproduce it, it comes down to two things. A deeplink whose config is just &lt;code&gt;{ "url": "https://mcp.publora.com/mcp" }&lt;/code&gt; — the address, no key, login goes through OAuth. And a server that serves valid metadata at &lt;code&gt;/.well-known/oauth-authorization-server&lt;/code&gt; instead of something homemade. If your client prints "OAuth provider initialized" and then quietly dies, put your money on this: it read your &lt;code&gt;.well-known&lt;/code&gt; and didn't find what it wanted.&lt;/p&gt;

&lt;p&gt;The big catalogs are still ahead of us, moving at their pace and not ours. The Cursor install works today. So if you're in Cursor and you use Publora, poke the button and tell me how it went — genuinely curious, especially if it breaks somewhere new.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>oauth</category>
      <category>cursor</category>
      <category>webdev</category>
    </item>
    <item>
      <title>LinkedIn gave everyone a 'this looks like AI' button. I won't press it.</title>
      <dc:creator>Eugeniya Ivanova</dc:creator>
      <pubDate>Tue, 04 Aug 2026 11:29:33 +0000</pubDate>
      <link>https://dev.to/eugeniya_ivanova_4a58eadc/linkedin-gave-everyone-a-this-looks-like-ai-button-i-wont-press-it-114m</link>
      <guid>https://dev.to/eugeniya_ivanova_4a58eadc/linkedin-gave-everyone-a-this-looks-like-ai-button-i-wont-press-it-114m</guid>
      <description>&lt;p&gt;This week LinkedIn &lt;a href="https://techcrunch.com/2026/07/30/linkedin-adds-a-button-to-report-ai-generated-slop/" rel="noopener noreferrer"&gt;rolled out&lt;/a&gt; a "Seems like AI slop" button. Tap it under someone's post and the post disappears from your feed, while your report goes off to train their filter. I heard about it through work — keeping an eye on what platforms are up to is part of the job — but I read it less as a marketer and more as someone who writes, and who now, apparently, walks around under suspicion.&lt;/p&gt;

&lt;p&gt;LinkedIn's logic isn't hard to follow. According to the AI detector Pangram, they have &lt;a href="https://www.pangram.com/blog/ai-in-your-feed" rel="noopener noreferrer"&gt;the highest share of AI content&lt;/a&gt; of any platform: roughly a third of short posts and over forty percent of long ones. The feed really is drowning, and handing people a valve is a reasonable move.&lt;/p&gt;

&lt;p&gt;It isn't the valve that bothers me. It's whose hands it got put in.&lt;/p&gt;

&lt;p&gt;The "this is AI" verdict used to come from an algorithm, and even that missed plenty. Pangram itself claims a 0.01% false-positive rate — which sounds impressive until you multiply it by volume. One hundredth of a percent of millions of posts is still thousands of real people getting labelled as bots. And that's the machine, the one somebody at least tested. Now the "this is AI" stamp has been handed to every passerby, and the cost of getting it wrong is zero. Tap, post gone, on with your coffee. You'll never even find out you were wrong — there's no feedback loop pointing back at you.&lt;/p&gt;

&lt;p&gt;I haven't touched the button once, and I don't even like AI text — the smooth filler I spent &lt;a href="https://dev.to/eugeniya_ivanova_4a58eadc/i-trained-an-ai-to-sound-like-me-then-spent-three-rounds-undoing-it-3k96"&gt;a whole post&lt;/a&gt; pulling apart last time. But disliking slop and swatting a person are different reflexes with different results. Someone posts the best thing they know how to write, and someone else decides it "looks like a bot." That's one reader gone. That isn't fighting slop. That's slapping the hands of someone who's only just reaching for the keyboard.&lt;/p&gt;

&lt;p&gt;Here's the personal part. I write with AI and I don't hide it, but the text is mine: a pile of rules, defined styles, years of tuning, every paragraph edited by hand. Slow — though it used to be slower. AI isn't my author, it's a tool I don't let grab the wheel. I wrote a whole article about that.&lt;/p&gt;

&lt;p&gt;Then something happened that made it feel a lot more personal. A while back, in a work chat, I answered a former colleague — a plain message, a couple of paragraphs. She reads it and says: "Wow, you sound just like GPT — fast and structured." A compliment. The catch is that about two minutes passed between her question and my answer. There wasn't even enough time to open ChatGPT, write a prompt, wait for an answer, and paste it back. It was just a human who's spent a long time working with words. And it read as a machine.&lt;/p&gt;

&lt;p&gt;And for a couple of days I was pleased about it — imagine, mistaken for GPT, fast and clean. Now it turns out that's exactly the thing I'm supposed to worry about. Same sentence. Same moment. A week later it had gone from a compliment to evidence, while nothing about me had changed. What changed is that there's now a button for it.&lt;/p&gt;

&lt;p&gt;Fluency is being read as forgery. Fast and clean means you didn't write it yourself, and there's now a button wired to that assumption.&lt;/p&gt;

&lt;p&gt;The irony is that AI was trained on well-edited human prose — which is exactly why it comes out so smooth and combed. And that smoothness is now how we try to spot it: we hunt for the fingerprints of a good editor, and we reliably find them on people who simply know how to write. Congratulations to all of us.&lt;/p&gt;

&lt;p&gt;My own way out of this is as old as it gets: divide and conquer. The machine is fast and a little too good — and sometimes not good at all, and that's the line I can still see and it can't. I handed it the posting across networks; works great — it's mechanical anyway. But writing is different. No matter how much I train it, it keeps sliding back into "we did it this way before, so this must be right." Except the new piece in front of it doesn't fit the old rules, and it doesn't notice that — I do. So here's the split: authorship and judgment stay with the human, logistics go to the machine. That's how I run it through &lt;a href="https://publora.com" rel="noopener noreferrer"&gt;Publora&lt;/a&gt; (I'm on the team): I write by hand, and the delivery across platforms goes to an agent. No AI writes my posts — it's on backup duty, in the corner, not at the wheel.&lt;/p&gt;

&lt;p&gt;How this ends, honestly, I don't know. For now I've stocked up on popcorn and I'm watching from two angles. Angle one: people start writing worse on purpose — breaking the rhythm, sprinkling in typos, watering it down, anything to avoid looking suspiciously smooth. A platform that penalizes quality, and a crowd cheerfully dumbing itself down to a C. Angle two: some just stop posting, because the fun runs thin when any passerby gets to weigh your fluency for "real or not." Which one wins, I won't guess. Popcorn's ready.&lt;/p&gt;

&lt;p&gt;I still won't press the button. Slop exists, no argument, plenty of it. But pressing something in two seconds, with zero accountability, that sorts people into "human / not human" — that I'm not up for.&lt;/p&gt;

&lt;p&gt;By the way, a huge secret: I wrote this article myself, by hand. The formatting and posting I handed to an agent — the writing was mine, the publishing wasn't, and that's exactly how I want it.&lt;/p&gt;

&lt;p&gt;Would you press it? And have you ever caught yourself deciding someone's fast, clean writing was too smooth to be real?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>writing</category>
      <category>career</category>
      <category>discuss</category>
    </item>
    <item>
      <title>You're writing for two readers, and one of them is a model</title>
      <dc:creator>Eugeniya Ivanova</dc:creator>
      <pubDate>Thu, 30 Jul 2026 11:28:17 +0000</pubDate>
      <link>https://dev.to/eugeniya_ivanova_4a58eadc/youre-writing-for-two-readers-and-one-of-them-is-a-model-4fp4</link>
      <guid>https://dev.to/eugeniya_ivanova_4a58eadc/youre-writing-for-two-readers-and-one-of-them-is-a-model-4fp4</guid>
      <description>&lt;p&gt;Every post you write now goes to two readers.&lt;/p&gt;

&lt;p&gt;The first is a person. They open it, read it, decide whether it was worth their time. Nothing new there — we've been writing for that reader forever.&lt;/p&gt;

&lt;p&gt;The second reader showed up recently, and it isn't a person. It's a model. It reads your text, works out what it's actually about, and decides whether to surface it or quote it in an answer. You're writing for both now. They want slightly different things — but less different than you'd fear.&lt;/p&gt;

&lt;p&gt;LinkedIn is the easiest example because they published how it works. Their &lt;a href="https://www.linkedin.com/blog/engineering/feed/engineering-the-next-generation-of-linkedins-feed" rel="noopener noreferrer"&gt;engineering post&lt;/a&gt; describes the new way content gets picked: instead of the old pile of separate pipelines, there's now an LLM retriever that turns every post and every profile into a vector of meaning, then pulls a couple thousand candidates out of millions by how close those meanings sit. That's the first point where a model reads your text. Before anything gets ranked, something has already worked out what you wrote about.&lt;/p&gt;

&lt;p&gt;Here's the part almost every write-up got wrong. The retrieval runs on an LLM, yes. But the final ranking of those candidates isn't done by a language model — it's a separate transformer called Feed-SR. In &lt;a href="https://arxiv.org/abs/2602.12354" rel="noopener noreferrer"&gt;their own paper&lt;/a&gt;, LinkedIn says plainly that they tried an LLM ranker and chose not to ship it: the transformer scored better and cost less to run. So "the feed is one giant LLM now" isn't what happened. The LLM sits exactly where it's decided whether your text gets considered at all — at the reading-for-meaning step.&lt;/p&gt;

&lt;p&gt;There's one catch worth mentioning. Online, everyone calls this system "360Brew" and recites its exact rules. 360Brew is a pre-production research model from a &lt;a href="https://arxiv.org/abs/2501.16450" rel="noopener noreferrer"&gt;separate paper&lt;/a&gt; whose every version on arXiv is marked withdrawn, and LinkedIn never confirmed it runs the feed. Their own &lt;a href="https://arxiv.org/abs/2602.12354" rel="noopener noreferrer"&gt;Feed-SR paper&lt;/a&gt; contradicts the story directly. Somewhere along the way, someone attached a catchy name to a real engineering paper, and everyone else repeated it. If a source can't get the name of the engine right, treat its "exact rules" as a guess too. Read the primary source, not the recap of a recap.&lt;/p&gt;

&lt;p&gt;The name isn't the important part. The shift is. The feed used to be gameable with mechanics — keywords, early-hour posting windows, pods trading likes. Now there's a model at the door that reads for meaning, and you can't fool it with the tricks that fooled a counter. It reads roughly the way an assistant reads a question you asked it.&lt;/p&gt;

&lt;p&gt;That's where the two readers start looking surprisingly similar. The assistant looking for the answer to a professional question and the feed retriever deciding whether to surface your post are doing the same job: reading the text for whether there's a specific point about a specific thing. What lands with the first lands with the second. Neither gets much from smooth, empty writing; both reach for text where it's clear what it's about and who it's for.&lt;/p&gt;

&lt;p&gt;This doesn't mean you should start writing for the algorithm. If anything, it points in the opposite direction. Writing for the machine used to mean writing worse: stuff in keywords, pad to length, staple on five hashtags. Now the machine reads like a reader, so writing for it means writing more clearly. Don't try to please the algorithm. Try to be understood. One concrete point beats ten blurry ones. An example someone can quote beats a paragraph of generalities. A topic you can be pinned to beats trying to appeal to everyone.&lt;/p&gt;

&lt;p&gt;There is one trap, though. Optimizing to "get quoted by a model" is a slippery road, and at the end of it waits exactly the AI slop I spent last time learning to scrub out — the confident, weightless prose that says nothing. The irony is that the model at the door doesn't like it either; it tells specific from smoothly-empty about as well as a person does. Push that too far and you end up writing for neither. The only thing that seems to work for both is having something specific to say.&lt;/p&gt;

&lt;p&gt;LinkedIn just happened to show how the system works first. Everything that retrieves and quotes you — search, assistants, feeds — is moving the same way, from "how many signals did you stack" to "what is this actually about." What people buried as SEO back in 2016 didn't die. Search just stopped counting and started reading.&lt;/p&gt;

&lt;p&gt;So yes, you've got two readers now. But write honestly and to the point, and the second one isn't extra work. It wants the same thing the first one does.&lt;/p&gt;

&lt;p&gt;Have you caught yourself writing with one eye on how a model will read it? Does the text come out better or worse for it?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>writing</category>
      <category>seo</category>
      <category>discuss</category>
    </item>
    <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>
