<?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>The portal said "Live." For a few hours the page kept saying the connector didn't exist.</title>
      <dc:creator>Eugeniya Ivanova</dc:creator>
      <pubDate>Thu, 24 Sep 2026 07:49:57 +0000</pubDate>
      <link>https://dev.to/eugeniya_ivanova_4a58eadc/the-portal-said-live-for-a-few-hours-the-page-kept-saying-the-connector-didnt-exist-m11</link>
      <guid>https://dev.to/eugeniya_ivanova_4a58eadc/the-portal-said-live-for-a-few-hours-the-page-kept-saying-the-connector-didnt-exist-m11</guid>
      <description>&lt;p&gt;I've submitted to just about every directory I could reach by now. Chrome, Firefox, Edge, Zapier, IFTTT, Figma, ChatGPT. Every time it's someone else's place: you show up, they check what you've brought, sometimes they send you back to fix it, sometimes they let you in. I've written enough about &lt;a href="https://dev.to/eugeniya_ivanova_4a58eadc/chrome-rejected-us-twice-firefox-approved-instantly-edge-is-still-thinking-e0e"&gt;review rounds and strange submission rules&lt;/a&gt; by now.&lt;/p&gt;

&lt;p&gt;And this week we submitted Publora to the official &lt;a href="https://claude.ai/directory/publora" rel="noopener noreferrer"&gt;Claude connectors directory&lt;/a&gt;. For some reason I'd been preparing for this one longer than any of the others. Not technically. The work was done. Apparently I had some feelings left over.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fifteen minutes I'd been walking toward the longest
&lt;/h3&gt;

&lt;p&gt;The submission itself took about fifteen minutes. A form, a button, done. Submitted yesterday, in the directory today. Behind those fifteen minutes was a month and a half of work by the whole team: thirteen pages of Anthropic documentation, checking the server against fifteen requirements with live requests, and manually running all eighteen tools against production.&lt;/p&gt;

&lt;p&gt;So why did the easiest submission bother me more than some of the genuinely horrible ones?&lt;/p&gt;

&lt;p&gt;I think it's because all the previous directories felt like other people's territory. Claude doesn't. I'm not an engineer; my job is getting people to use our product, and almost everything I've built this year I've built with Claude somewhere next to me. It's my pocket techie: it writes the code, I lead, argue and make it redo things. Twenty articles, a dozen and a half integrations, all those submissions. And now Publora is sitting in Claude's own directory. That felt different.&lt;/p&gt;

&lt;p&gt;That's enough sentiment. There were also several stupid things that cost us time, and those are probably more useful if you're submitting a connector yourself.&lt;/p&gt;

&lt;h3&gt;
  
  
  The portal says one thing, reality says another
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The main thing I'd keep in mind: a green status in the portal doesn't necessarily mean the thing next to it is already working.&lt;/strong&gt; We found that out in three different places.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Live in the directory," and the page is empty.&lt;/strong&gt; We hit publish and the portal immediately reported "live." The listing page, meanwhile, spent several hours returning "This connector doesn't exist." The View listing button went to the same page, and search found only our temporary connection. We'd already written a support email and, luckily, hadn't sent it yet. A few hours later the listing appeared on its own. If you see the same thing, give it some time before writing to support.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The portal decides the server is authless when it isn't.&lt;/strong&gt; At the authorization step it checks &lt;code&gt;tools/list&lt;/code&gt;. Ours returns the tool list without a token, so the portal announced: "Server requires no auth to list and call tools." The first half is true. The second isn't. You can see our tools without authorization, but you can't call any of them. We still had to select OAuth manually.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The docs disagreed with the live form in four places.&lt;/strong&gt; The &lt;a href="https://claude.com/docs/connectors/building/submission" rel="noopener noreferrer"&gt;docs&lt;/a&gt; promised eleven steps; we got ten. The icon field was described as a file upload but turned out to want a URL. The best one was the listing address: the docs use &lt;code&gt;claude.ai/directory/connectors/&amp;lt;slug&amp;gt;&lt;/code&gt;, while the actual listing is &lt;code&gt;claude.ai/directory/&amp;lt;slug&amp;gt;&lt;/code&gt;. The old version even appeared in the approval email. We nearly copied it into our own documentation because apparently an email can still outrank the browser in my head.&lt;/p&gt;

&lt;h3&gt;
  
  
  The rake half the ecosystem steps on
&lt;/h3&gt;

&lt;p&gt;One deserves its own section, because I went looking to see whether we'd done something stupid. This time, at least, we had company.&lt;/p&gt;

&lt;p&gt;The portal flagged all eighteen of our tools as "missing a title." All eighteen had titles. The problem is that the MCP spec has two places for &lt;code&gt;title&lt;/code&gt;: top-level &lt;code&gt;Tool.title&lt;/code&gt;, &lt;a href="https://modelcontextprotocol.io/specification/2025-06-18/server/tools" rel="noopener noreferrer"&gt;added in summer 2025&lt;/a&gt;, and the older &lt;code&gt;annotations.title&lt;/code&gt;. We used the newer field. &lt;strong&gt;The Claude directory was checking &lt;code&gt;annotations.title&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This has caught other people too. A &lt;a href="https://github.com/PrefectHQ/fastmcp/pull/5217" rel="noopener noreferrer"&gt;pull request to FastMCP&lt;/a&gt; describes the same problem: its titles were going only into the top-level field, so the Claude connectors directory saw the tools as nameless. If the portal says your tools are missing titles and you can clearly see the titles in the schema, check &lt;code&gt;annotations.title&lt;/code&gt; before doing anything more dramatic. That string is also what users see in the confirmation dialog, so they get "Create post" instead of &lt;code&gt;create_post&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  A couple of smaller rakes, also worth taking with you
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The form draft doesn't save.&lt;/strong&gt; We went through all ten steps, clicked "Save and exit," and lost the lot. When we came back, the form was empty and the dashboard said we hadn't submitted any servers. "Draft saved in this tab" is apparently very literal. I'd do the form in one sitting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The "Keep your personal account separate" checkbox when creating an organization.&lt;/strong&gt; Without it, your personal subscription is cancelled and your chat history moves into the organization, with no way to move it back. It's an easy checkbox to skim past and not a particularly easy decision to undo.&lt;/p&gt;

&lt;h3&gt;
  
  
  About what I learned myself
&lt;/h3&gt;

&lt;p&gt;Separately, because this part was my problem rather than Anthropic's. Along the way I put four things into our blockers: a network supposedly missing from the test account, a strange 403, dead tokens and a parameter I thought we had to change. None survived an actual live check. In several cases I'd looked at one field in the database, decided what it meant and promoted the guess to a problem.&lt;/p&gt;

&lt;p&gt;There was a good example in the other direction too. At one point I entered the wrong plan, the one I thought was required for submission. Claude accepted that as a fact, built the rest of the reasoning around it and very confidently helped me proceed in the wrong direction. Which was fair enough. I had given it bad information and then asked it to reason from there.&lt;/p&gt;

&lt;p&gt;I'm getting better at noticing this when we work together: when Claude has started building on an assumption, when it's doing something I didn't actually ask for, and when I need to stop and check the boring fact underneath all the reasoning. The same applies to me. My "I think there's a defect here" can become a blocker in a document surprisingly quickly. Claude's guess can do the same. Neither becomes more factual because it has been written down neatly.&lt;/p&gt;

&lt;h3&gt;
  
  
  What to do with this yourself
&lt;/h3&gt;

&lt;p&gt;Publora is &lt;a href="https://claude.ai/directory/publora" rel="noopener noreferrer"&gt;in the Claude directory&lt;/a&gt; now. You can connect it and create posts for ten social networks from the same chat where you're already working, instead of moving the text into another app afterward.&lt;/p&gt;

&lt;p&gt;The part I'd still be careful with is delegation. I've &lt;a href="https://dev.to/eugeniya_ivanova_4a58eadc/the-agent-posted-successfully-to-the-wrong-account-3kf3"&gt;written about this before&lt;/a&gt;, but it keeps coming up: vague instructions give Claude room to fill things in. If you want a post published, tell it what to publish, where and when. If you want a draft, say draft. I don't rely on Claude to guess which parts of a publishing decision I meant to leave unsaid.&lt;/p&gt;

&lt;p&gt;Looking back at the things I've built this year, there's a slightly ridiculous amount of technical work for someone who still introduces herself by saying she's not an engineer. Claude has been involved in most of it. So seeing Publora in its directory was nice in a way I hadn't expected.&lt;/p&gt;

&lt;p&gt;The actual submission took fifteen minutes. Getting to the point where those fifteen minutes were possible took considerably longer.&lt;/p&gt;




&lt;p&gt;I wrote this one with Claude as well, which seemed only fair given where it ended up. I told it what happened, it drafted, and I cut whatever hadn't actually happened.&lt;/p&gt;

&lt;p&gt;Have you ever spent months getting somewhere and then found the actual moment itself was almost suspiciously uneventful?&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>claude</category>
      <category>api</category>
    </item>
    <item>
      <title>I was sure Figma would reject my cover art. The email asked about something else.</title>
      <dc:creator>Eugeniya Ivanova</dc:creator>
      <pubDate>Mon, 21 Sep 2026 07:32:45 +0000</pubDate>
      <link>https://dev.to/eugeniya_ivanova_4a58eadc/i-was-sure-figma-would-reject-my-cover-art-the-email-asked-about-something-else-2j4k</link>
      <guid>https://dev.to/eugeniya_ivanova_4a58eadc/i-was-sure-figma-would-reject-my-cover-art-the-email-asked-about-something-else-2j4k</guid>
      <description>&lt;p&gt;A few years ago, my Figma skills were roughly limited to whatever I could make from templates designer friends gave me. Sometimes I made silly pictures for colleagues. A normal user, basically: I can do a few things, and if I can't, I probably don't need to be there.&lt;/p&gt;

&lt;p&gt;Then I changed jobs, and somehow this escalated. Sometimes I fix a layout myself, sometimes I make an agent do it because spending half an hour figuring out where Figma hid one particular effect is usually beyond both my time and my patience. And then I ended up in a fairly strange place: I wasn't just using other people's Figma plugins anymore. I'd &lt;a href="https://www.figma.com/community/plugin/1673632240752253298/publora-publish-to-social" rel="noopener noreferrer"&gt;made my own&lt;/a&gt;. It lets you send a design from Figma straight to your social accounts without exporting it and dragging it through three other apps first.&lt;/p&gt;

&lt;p&gt;What a wonderful world. You never know where it'll take you.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where I got stuck, and it wasn't where I expected
&lt;/h3&gt;

&lt;p&gt;Writing the plugin wasn't the scary part. I got stuck on the pictures.&lt;/p&gt;

&lt;p&gt;To publish a plugin, Figma asks for an icon, a cover and screenshots. The cover stopped me. Make it myself in Figma, where I am, let's put it gently, not very good? Give it to an agent? Do both and then spend ages choosing between things I'm not qualified to judge? I spent an embarrassing amount of time on this.&lt;/p&gt;

&lt;p&gt;The problem was stupid, but real. I was about to submit a plugin to a platform full of designers. Actual designers. And here I come with my cover. People will look at it. People will know.&lt;/p&gt;

&lt;p&gt;In the end, Claude made the cover. Simple, clean, no attempt on my part to demonstrate the designer apparently hiding somewhere inside me. I submitted the plugin and waited to be told what was wrong with the pictures.&lt;/p&gt;

&lt;h3&gt;
  
  
  The email came. About something else.
&lt;/h3&gt;

&lt;p&gt;Figma's review was nothing like the others I'd been through. Chrome took two rounds. Adobe sent us back with changes too. Zapier had eight. By then I'd come to think of moderation as a kind of correspondence where somebody explains what you've done wrong and you write back when you've fixed it.&lt;/p&gt;

&lt;p&gt;Figma said nothing. Then an email arrived: send us a video showing how the plugin works.&lt;/p&gt;

&lt;p&gt;That was it.&lt;/p&gt;

&lt;p&gt;Not "the cover is weak." Not "the icon needs work." Not even "who let you in here." They wanted to see that the plugin did what it was supposed to do.&lt;/p&gt;

&lt;p&gt;We recorded the video and sent it. Silence again. Then the plugin appeared in the Community. It even has a couple of users already, which I particularly like because it means we're no longer the only people using the thing we made.&lt;/p&gt;

&lt;p&gt;Only after that did I read &lt;a href="https://help.figma.com/hc/en-us/articles/360039958914-Plugin-and-widget-review-guidelines" rel="noopener noreferrer"&gt;Figma's review rules&lt;/a&gt;, which would obviously have been more useful before I started worrying. There's nothing in there about how the cover looks. They review whether the plugin is finished, whether it crashes, whether it does what the listing says, whether it's safe with someone's files. The only line that touches the pictures at all asks for an accurate description, screenshot, or previews, "so that the users will not be surprised by any hidden functionality." Not a good cover. An honest one.&lt;/p&gt;

&lt;p&gt;So I'd spent several days worrying about a test nobody was planning to give me.&lt;/p&gt;

&lt;h3&gt;
  
  
  And this is where it gets a little strange
&lt;/h3&gt;

&lt;p&gt;I didn't become a designer along the way. I didn't become an engineer either. But there's a plugin in the Figma Community now, and it has a perfectly decent cover.&lt;/p&gt;

&lt;p&gt;A few years ago, "make a cover for a plugin" would have meant learning how to make covers first. Now I can explain what I need to an agent, get a few options, throw away the terrible ones and keep something decent. This does not make me a designer. It does, however, leave me with a cover.&lt;/p&gt;

&lt;p&gt;I'm still not entirely sure what I think about that. Taste and a good eye obviously still matter: if you're given five bad options, somebody has to notice they're bad. But I also quite like living in a world where needing one particular effect doesn't automatically mean watching a forty-minute tutorial about it.&lt;/p&gt;

&lt;p&gt;My actual job is getting people to use our product. I'm not a designer or an engineer. But somewhere in the Figma Community there's now our plugin, with a cover I couldn't have made myself.&lt;/p&gt;

&lt;p&gt;Well. Apparently I can make Figma plugins now.&lt;/p&gt;




&lt;p&gt;I wrote this one with Claude too, which feels about right for a post that's partly about not making things myself. I described what happened, it drafted, and I cut the parts that weren't true.&lt;/p&gt;

&lt;p&gt;Has AI ever dropped you somewhere the old rules say you don't belong?&lt;/p&gt;

</description>
      <category>figma</category>
      <category>ai</category>
      <category>design</category>
      <category>career</category>
    </item>
    <item>
      <title>I didn't come to Claude through code. I came through a 10 a.m. routine.</title>
      <dc:creator>Eugeniya Ivanova</dc:creator>
      <pubDate>Fri, 18 Sep 2026 07:28:48 +0000</pubDate>
      <link>https://dev.to/eugeniya_ivanova_4a58eadc/i-didnt-come-to-claude-through-code-i-came-through-a-10-am-routine-3e14</link>
      <guid>https://dev.to/eugeniya_ivanova_4a58eadc/i-didnt-come-to-claude-through-code-i-came-through-a-10-am-routine-3e14</guid>
      <description>&lt;p&gt;Most people I know got to Claude through code, which is reasonable enough considering the name. I got there because at ten in the morning I wanted somebody else to check my numbers.&lt;/p&gt;

&lt;p&gt;I'm not an engineer. My job is getting people to use our product, which means I spend a surprising amount of time looking at numbers, looking at people, and then looking at the numbers again because I no longer trust what I saw the first time. I started with Claude Desktop, then tried Cowork, and eventually found &lt;a href="https://docs.claude.com/en/docs/claude-code/routines" rel="noopener noreferrer"&gt;routines&lt;/a&gt;. I have three now. None is particularly impressive, which is probably why I actually use them.&lt;/p&gt;

&lt;h3&gt;
  
  
  The 10 a.m. check
&lt;/h3&gt;

&lt;p&gt;The serious Publora analytics live in Claude Code in VSCode. That's where the keys are, the detailed data, revenue, and all the things capable of ruining an otherwise decent morning. I can go in there and dig properly, but the problem is that I can always dig properly. There is always another cohort to check, another strange number, another question that suddenly seems urgent because I've opened the file containing it. If I do that every morning, there goes the morning.&lt;/p&gt;

&lt;p&gt;So at 10 a.m. Claude sends me a Publora brief: tasks, analytics, meetings, anything that looks unusual. I've normally been at my desk since eight, so by then I've had coffee, answered email and acquired enough emotional stability to look at analytics. If something moved, I open VSCode and investigate. If everything looks normal, I leave it alone. Before this routine, apparently I needed two hours of analysis every now and then to discover that everything was fine.&lt;/p&gt;

&lt;h3&gt;
  
  
  The file where future posts go to wait
&lt;/h3&gt;

&lt;p&gt;The second routine runs Monday, Wednesday and Friday. It looks around my usual topics, finds two or three things worth writing about and puts them in &lt;code&gt;content-backlog.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The file turned out to matter. At first I had Claude send me ideas, which produced a very nice list that I would read, approve of and never see again. Now the ideas accumulate in one place. By Friday there are several possible posts in there. Some are bad, some seemed good on Monday and make no sense by Friday, and usually a couple survive.&lt;/p&gt;

&lt;p&gt;That's all I need from it. I don't want Claude writing my posts while I sleep. I want to sit down to write and not spend the first forty minutes discovering that apparently I have never had a thought in my life.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hacker News without going to Hacker News
&lt;/h3&gt;

&lt;p&gt;The third one is technically a &lt;a href="https://docs.claude.com/en/docs/claude-code/scheduled-tasks" rel="noopener noreferrer"&gt;scheduled task&lt;/a&gt; rather than a routine. It gives me a Hacker News digest twice a day. This exists because I know what happens when I tell myself I'll check the news when I have time: either I don't, or I open Hacker News to read one thing, then another, then somebody is wrong in the comments, and forty minutes later I'm extremely well informed about something I wasn't working on.&lt;/p&gt;

&lt;p&gt;Now the digest arrives by itself. I skim it, open whatever actually looks useful and occasionally find something I want to comment on. I remain approximately aware of the world without having to visit quite so much of it.&lt;/p&gt;

&lt;h3&gt;
  
  
  My routines stop when I close the laptop
&lt;/h3&gt;

&lt;p&gt;There is one limitation: these routines are local, so my computer has to be awake and online. Claude Code also has cloud Routines that can run while the laptop is closed, which means you can wake up and the work is already there. People quite reasonably describe this as an advantage.&lt;/p&gt;

&lt;p&gt;I understand them intellectually. Personally, I'm an anxious person, and the idea that Claude has been awake since six doing things on my behalf while I was asleep does not relax me. I'd wake up wondering what it had done. Mine start working when I do. I'm at the desk, Claude is at the desk, and nobody has been getting creative overnight. This arrangement suits me.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I actually gave it
&lt;/h3&gt;

&lt;p&gt;None of these routines does anything spectacular. One checks whether something in the numbers needs my attention, one keeps a file of things I might write about, and one reads Hacker News so I don't accidentally spend forty minutes reading Hacker News myself.&lt;/p&gt;

&lt;p&gt;I still make the decisions, which unfortunately seems to be most of my job. What disappeared was the need to remember to go and check all this stuff myself. And I set the whole thing up in plain English, without writing code, which is mildly funny considering how long the word "Code" made me assume this wasn't for me.&lt;/p&gt;

&lt;p&gt;Now I'm much more interested in the boring things people put on routines than the impressive ones.&lt;/p&gt;




&lt;p&gt;I wrote this one with Claude as well: I described what the three routines actually do, it drafted, and I cut the parts that weren't true. Which is more or less the arrangement everywhere else in this post.&lt;/p&gt;

&lt;p&gt;What's the stupid recurring thing you got tired of doing yourself?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>automation</category>
      <category>claude</category>
    </item>
    <item>
      <title>IFTTT accepted our service on the first try. The flow broke on the first redirect.</title>
      <dc:creator>Eugeniya Ivanova</dc:creator>
      <pubDate>Wed, 16 Sep 2026 15:30:00 +0000</pubDate>
      <link>https://dev.to/eugeniya_ivanova_4a58eadc/ifttt-accepted-our-service-on-the-first-try-the-flow-broke-on-the-first-redirect-4fjd</link>
      <guid>https://dev.to/eugeniya_ivanova_4a58eadc/ifttt-accepted-our-service-on-the-first-try-the-flow-broke-on-the-first-redirect-4fjd</guid>
      <description>&lt;p&gt;Last week I was bringing Publora onto IFTTT. Our server was already live, OAuth was working, and the API was there. I expected most of the work to be filling in their application and getting through review.&lt;/p&gt;

&lt;p&gt;The application passed on the first try, in under a day instead of the estimated one to two weeks. The actual integration broke on the first authorization redirect.&lt;/p&gt;

&lt;p&gt;Here's what I ran into.&lt;/p&gt;

&lt;h2&gt;
  
  
  The OAuth that didn't line up
&lt;/h2&gt;

&lt;p&gt;The first problem showed up immediately.&lt;/p&gt;

&lt;p&gt;IFTTT sends plain OAuth2: authorization code, no PKCE. Our authorization server requires PKCE. Their redirect arrives without a &lt;code&gt;code_challenge&lt;/code&gt;, so our server rejects it.&lt;/p&gt;

&lt;p&gt;I tried the request in exactly the format IFTTT sends:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET https://mcp.publora.com/authorize
  ?client_id=...&amp;amp;response_type=code&amp;amp;scope=ifttt&amp;amp;state=...&amp;amp;redirect_uri=...

-&amp;gt; 302 ...?error=invalid_request
   "expected string, received undefined" on code_challenge
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add a &lt;code&gt;code_challenge&lt;/code&gt; to the same request and it reaches the login screen normally. So there wasn't much mystery left: IFTTT doesn't send something our authorization server requires.&lt;/p&gt;

&lt;p&gt;I could either make PKCE optional on our server for IFTTT or put something between the two. I went with the second option.&lt;/p&gt;

&lt;p&gt;I added a small Cloudflare Worker between IFTTT and Publora. To IFTTT, it looks like the plain OAuth2 provider it expects: no PKCE and a non-expiring token. On the Publora side, the Worker runs the PKCE flow itself, generating the verifier and challenge before sending the request to our authorization server.&lt;/p&gt;

&lt;p&gt;That also gave me somewhere to handle the rest of the differences between the two APIs. IFTTT expects endpoints and responses in its own format, while Publora already has its own API. The Worker translates between them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turning posts into an event feed
&lt;/h2&gt;

&lt;p&gt;IFTTT treats a trigger as a feed rather than a current state: up to fifty recent events, newest first, each with its own &lt;code&gt;meta.id&lt;/code&gt; and &lt;code&gt;meta.timestamp&lt;/code&gt; in seconds.&lt;/p&gt;

&lt;p&gt;For Publora that means one post group may need to become several events, one for each channel. Otherwise the &lt;code&gt;meta.id&lt;/code&gt; isn't unique:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;platforms&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;account&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;label&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;connections&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;platformId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;network&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;occurred_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;postGroupId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;platformId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;seconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's another requirement they test separately: every trigger needs to return at least three events during testing, so it behaves like a feed rather than a state sensor. An empty or short response doesn't pass.&lt;/p&gt;

&lt;p&gt;I also replaced hourly polling with realtime notifications. A Publora webhook is created when the connection is set up, and when an event arrives the Worker calls IFTTT's Realtime API. It doesn't send the event data there; it just tells IFTTT there's something new, and IFTTT fetches the feed itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five things I couldn't find in their docs
&lt;/h2&gt;

&lt;p&gt;This is where I spent most of the time. If you're building an IFTTT service, these are worth knowing before you start running their tests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;test/setup&lt;/code&gt; endpoint is required, but I couldn't find it in the docs.&lt;/strong&gt; Their automated tests hit it second. There is a sample response in the dashboard under View scaffold JSON: it needs a &lt;code&gt;queries&lt;/code&gt; section, and &lt;code&gt;media_url&lt;/code&gt; can't be an empty string. The method isn't specified there, so ours accepts both GET and POST.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ingredient slugs can double without showing it in the UI.&lt;/strong&gt; The form generates a slug while you're typing the field name, and entering your own can leave you with things like &lt;code&gt;contentcontent&lt;/code&gt; and &lt;code&gt;occurred_atoccurred_at&lt;/code&gt;. The resulting slug isn't visible in the form. I only found it in the service export under Tools, Export. A good chunk of my failed tests came from this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The UI didn't show me why tests were failing.&lt;/strong&gt; I could expand the failed result row, but it was empty. The actual results were available through these endpoints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST /services/&amp;lt;slug&amp;gt;/publish/endpoint_session   -&amp;gt; session_id
GET  /services/&amp;lt;slug&amp;gt;/publish/endpoint_test_results?offset=N&amp;amp;session_id=...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The results come in chunks, with &lt;code&gt;suite_end&lt;/code&gt; marking the end. That's where I eventually found the individual failed checks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A request with no fields needs to return a proper 400.&lt;/strong&gt; They test requests with &lt;code&gt;triggerFields&lt;/code&gt; missing and with required keys missing inside it. The response needs an &lt;code&gt;errors&lt;/code&gt; array. I initially supplied defaults in some of these cases, which made the tests fail.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The cursor is mandatory when results are limited.&lt;/strong&gt; If you return only part of the feed because of a limit, the response also needs a &lt;code&gt;cursor&lt;/code&gt;. Without it, the test fails.&lt;/p&gt;

&lt;h2&gt;
  
  
  Numbers
&lt;/h2&gt;

&lt;p&gt;Their run has 535 checks. I eventually got it to zero failures, after spending considerably more time than I'd like to admit on doubled slugs and test results hidden behind an empty UI row.&lt;/p&gt;

&lt;p&gt;There are also two launch requirements I hadn't found in the docs: twelve published applets and four service admins. You can submit the service without them, but you need them before launch.&lt;/p&gt;

&lt;p&gt;I submitted on September 7 and the review passed on September 8, so under a day against the estimated one to two weeks.&lt;/p&gt;

&lt;p&gt;The Worker ended up at about 700 lines of TypeScript and a 19.8 KB bundle.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd check first next time
&lt;/h2&gt;

&lt;p&gt;If you're doing an IFTTT integration, check the generated slugs in the service export rather than trusting the form. If an automated test fails without an explanation in the UI, look at &lt;code&gt;endpoint_test_results&lt;/code&gt;. And before running the full suite, make sure you have &lt;code&gt;test/setup&lt;/code&gt;, cursors on limited responses, and proper 400 responses for missing fields.&lt;/p&gt;

&lt;p&gt;For OAuth, I was glad I didn't make PKCE optional in Publora just to accommodate this integration. The Worker was already needed to translate IFTTT's API format, so handling the OAuth difference there kept the change isolated too.&lt;/p&gt;

&lt;p&gt;I built it with Claude: it wrote most of the TypeScript, while I tested the requests in IFTTT's format and worked out where the two sides disagreed. Most of the time went into finding those disagreements, especially the ones I couldn't see from the dashboard.&lt;/p&gt;

&lt;p&gt;The listing is here: &lt;a href="https://ifttt.com/publora" rel="noopener noreferrer"&gt;https://ifttt.com/publora&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you've integrated with IFTTT before and found another undocumented test, I'd like to know which one.&lt;/p&gt;

</description>
      <category>ifttt</category>
      <category>oauth</category>
      <category>api</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I gave an agent my posting history. It found a promise I never made.</title>
      <dc:creator>Eugeniya Ivanova</dc:creator>
      <pubDate>Tue, 08 Sep 2026 07:10:41 +0000</pubDate>
      <link>https://dev.to/eugeniya_ivanova_4a58eadc/i-gave-an-agent-my-posting-history-it-found-a-promise-i-never-made-4n62</link>
      <guid>https://dev.to/eugeniya_ivanova_4a58eadc/i-gave-an-agent-my-posting-history-it-found-a-promise-i-never-made-4n62</guid>
      <description>&lt;p&gt;Every social platform rewards the same thing, and it isn't a secret: showing up regularly. The hard part isn't writing. It's deciding what to say today, and on plenty of days there either isn't an answer or you don't feel like hunting for one.&lt;/p&gt;

&lt;p&gt;Someone I was talking to on LinkedIn last week told me they stick with their scheduling tool mostly because of the post templates. When they run out of ideas, the templates give them somewhere to start. That surprised me. I would have guessed people got stuck on the writing, not on choosing the topic.&lt;/p&gt;

&lt;p&gt;Asking an agent doesn't really fix that either, at least not in the usual setup. You say "write me a post," it gives you one draft based on its first guess, and if you're not convinced, there's often nothing specific to react to. You end up rewriting it yourself, which rather defeats the point.&lt;/p&gt;

&lt;p&gt;So this week I built a small skill around one very narrow idea: don't give me a draft first. Give me three angles and let me pick. Three options I can answer with a number.&lt;/p&gt;

&lt;p&gt;It works now. The first real run didn't. It failed three times before it gave me anything useful, and all three failures came from the same place: the agent was reading my own data and treating its interpretation as fact.&lt;/p&gt;

&lt;h3&gt;
  
  
  It read my test posts as things my audience saw
&lt;/h3&gt;

&lt;p&gt;The skill starts by calling &lt;code&gt;list_posts&lt;/code&gt; and reading the last twenty. It's looking for two things: topics I've already covered, so it doesn't suggest the same thing again, and threads I've left hanging. A launch with no follow-up. A question I asked and never came back to. Those are often more useful than inventing a new topic because the context already exists and I usually have the material.&lt;/p&gt;

&lt;p&gt;It found one. Two posts on my LinkedIn from late July said Publora was coming to Zapier. Nothing after that. So it suggested: finish the thread, you promised this a month ago and never said how it turned out.&lt;/p&gt;

&lt;p&gt;Except I hadn't promised anything.&lt;/p&gt;

&lt;p&gt;Those posts were Zapier review artifacts. To submit an app there, you have to run every trigger and action inside a live Zap, switch it on, and leave a successful run in the history. That means my account contains posts with names like &lt;code&gt;Zapier validation — update target&lt;/code&gt;. They look like normal posts in the same list, and the agent had no idea they weren't meant for an audience.&lt;/p&gt;

&lt;p&gt;I added a filter: short posts, near-duplicates minutes apart, anything containing &lt;em&gt;test&lt;/em&gt; or &lt;em&gt;validation&lt;/em&gt;. But the more useful fix was a rule:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Never state an inference from history as a fact.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;"You promised X and never followed up" sounds confident, but it's still an inference from a list that includes QA junk. The agent should say what it found and ask whether it understood it correctly. Once I corrected it, the whole session changed direction in one message. If I hadn't noticed, I could easily have published a post referring to a promise nobody ever saw.&lt;/p&gt;

&lt;h3&gt;
  
  
  It could only see what went through the product
&lt;/h3&gt;

&lt;p&gt;Once we had the right angle, the skill asked for the one fact it couldn't know: what actually happened with Zapier. I told it we were live but still in beta.&lt;/p&gt;

&lt;p&gt;What it didn't ask was whether I'd already written about it somewhere else. I had. That morning I'd published a four-minute Dev.to article with the eight rounds of review, the REST Hooks, and the JavaScript I ended up putting in a field labeled "label". None of that existed in Publora, so the skill had no way to know about it.&lt;/p&gt;

&lt;p&gt;That limitation is obvious in theory: an agent only knows the data you give it. In practice, it's easy to forget until it starts repeating something you've already published elsewhere.&lt;/p&gt;

&lt;p&gt;So now, whenever it asks for a missing fact, it also asks whether I've already written about the topic somewhere else: article, changelog, release notes, whatever.&lt;/p&gt;

&lt;h3&gt;
  
  
  The slop checker passed a draft that was still slop
&lt;/h3&gt;

&lt;p&gt;I run every outgoing text through a script that flags machine-sounding writing: vocabulary, repeated structures, the usual tells that become easy to spot once you've collected enough of them.&lt;/p&gt;

&lt;p&gt;The draft passed.&lt;/p&gt;

&lt;p&gt;I showed it to a colleague anyway, and she said she could still see AI slop in it. She was right.&lt;/p&gt;

&lt;p&gt;Three of the strongest lines had been lifted almost directly from my Dev.to article from that morning. "The requirement I reread three times, certain I'd misunderstood." Fine line on its own. Less fine when the same person has already seen it six hours earlier. No vocabulary checker is going to catch that.&lt;/p&gt;

&lt;p&gt;The other problems were structural. A withheld hook: "with a Beta tag, which I'll get to." A neat paradox: "you have to prove the app is being used before it's allowed to exist." A three-item list whose last item worked mostly because of the rhythm. The word "literally". None of those is automatically bad. Together, in a short post, they started sounding very familiar.&lt;/p&gt;

&lt;p&gt;The rewrite was 720 characters. The version that passed the script was 1198.&lt;/p&gt;

&lt;p&gt;So I kept the checker, but moved it earlier in the process. It catches the things that exist inside the text. What it can't see is everything around the text: what I published yesterday, who is reading, or whether I've stacked too many individually reasonable choices into something that sounds generated.&lt;/p&gt;

&lt;p&gt;I'd still put a checker somewhere between draft and publish. It costs very little once it exists, catches the obvious layer, and doesn't get tired at six in the evening. Build one, borrow one, or use a checklist. Just don't mistake it for the whole review.&lt;/p&gt;

&lt;h3&gt;
  
  
  What it looks like now
&lt;/h3&gt;

&lt;p&gt;There are eight categories and forty angles. Every angle has a &lt;strong&gt;core&lt;/strong&gt;: one fact that has to come from the person using the skill, whether that's the mistake, the number, or the tool name. The instructions repeat this in several places because inventing that core is the easiest way to produce something plausible and false under someone's name.&lt;/p&gt;

&lt;p&gt;There are also four reference files behind the angles: what the first two lines need to do before the feed collapses the rest, how to infer voice from existing posts instead of asking someone to describe their own voice, the tells a script can't catch, and how the same angle changes between LinkedIn and X.&lt;/p&gt;

&lt;p&gt;It stops at the draft. Nothing publishes automatically.&lt;/p&gt;

&lt;p&gt;It's MIT, it works on its own, and connecting Publora lets it read your feed and schedule the result: &lt;a href="https://github.com/publora-team/publora-post-ideas" rel="noopener noreferrer"&gt;publora-team/publora-post-ideas&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;For the record, I'm not an engineer. My job is getting people to use our product, and I built this with Claude: it wrote the code, I led, tested, and sent it back when it got things wrong. Most of the useful work ended up being in those three failures, because each one showed me a rule I hadn't written yet.&lt;/p&gt;

&lt;p&gt;Has an agent ever been confidently wrong about your own data? I'm curious what it misunderstood, and what you changed after that.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>writing</category>
      <category>socialmedia</category>
    </item>
    <item>
      <title>My MCP integration got rejected. Almost nothing in the server had to change.</title>
      <dc:creator>Eugeniya Ivanova</dc:creator>
      <pubDate>Mon, 07 Sep 2026 07:54:22 +0000</pubDate>
      <link>https://dev.to/eugeniya_ivanova_4a58eadc/my-mcp-integration-got-rejected-almost-nothing-in-the-server-had-to-change-npb</link>
      <guid>https://dev.to/eugeniya_ivanova_4a58eadc/my-mcp-integration-got-rejected-almost-nothing-in-the-server-had-to-change-npb</guid>
      <description>&lt;p&gt;In July I set out to add our MCP server to the ChatGPT app directory. The server was already live, running in Claude and Cursor, with OAuth working. Submitting looked like a formality: describe what already exists, fill in the application, hit Submit.&lt;/p&gt;

&lt;p&gt;A month and one rejection later, almost nothing had changed in the server itself. It had been working the whole time. What we ended up fixing was everything around it: how ChatGPT discovers the tools, what the scanner expects, what we'd written in the descriptions, and whether a reviewer could actually log in.&lt;/p&gt;

&lt;p&gt;Here's what caught me.&lt;/p&gt;

&lt;h3&gt;
  
  
  The tool list that freezes solid
&lt;/h3&gt;

&lt;p&gt;The first problem came before submission. I connected the server to ChatGPT — the connector was created, but it showed "no actions available." Zero tools. In Claude and Cursor the same tools loaded fine.&lt;/p&gt;

&lt;p&gt;I spent a while looking at SSE and caching. Neither was the problem. ChatGPT reads the tool list when the app is created, and if the server returns zero at that point, the app can get stuck there. "Refresh" is disabled at zero and "Save" is greyed out. In my case, the way out was to delete the app and create it again.&lt;/p&gt;

&lt;p&gt;The server returned zero because it required a token for &lt;code&gt;tools/list&lt;/code&gt; itself. Claude logs in first and fetches the list afterwards, which is why we'd never seen the problem there. ChatGPT fetches the list before authorization, gets nothing, and turns to stone. So now I check one thing before doing anything else: &lt;code&gt;tools/list&lt;/code&gt; needs to return 200 with the tools and no token.&lt;/p&gt;

&lt;h3&gt;
  
  
  Annotations the scanner demands and the spec doesn't
&lt;/h3&gt;

&lt;p&gt;Next, OpenAI's scanner went through every tool and required explicit &lt;code&gt;readOnlyHint&lt;/code&gt;, &lt;code&gt;openWorldHint&lt;/code&gt; and &lt;code&gt;destructiveHint&lt;/code&gt; values. Four read-only tools didn't have &lt;code&gt;destructiveHint&lt;/code&gt;, which made sense according to &lt;a href="https://modelcontextprotocol.io/specification/2025-06-18/server/tools" rel="noopener noreferrer"&gt;the MCP spec&lt;/a&gt;: it only matters when &lt;code&gt;readOnlyHint: false&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The scanner wanted it anyway. We added &lt;code&gt;destructiveHint: false&lt;/code&gt; to those tools and wrote justifications for forty-odd annotation values. The tools themselves didn't change. We just had to make explicit something the spec allowed us to leave implied.&lt;/p&gt;

&lt;h3&gt;
  
  
  The domain challenge that wants a bare string and lives forever
&lt;/h3&gt;

&lt;p&gt;A small one that's easy to waste time on: OpenAI's domain verification expects the endpoint to return a bare token string. No JSON, no quotes. Return &lt;code&gt;{"token": "..."}&lt;/code&gt; and it fails.&lt;/p&gt;

&lt;p&gt;It also isn't a one-time verification endpoint. It has to stay there in production, answering the same way afterwards. So we now have a tiny endpoint with one permanent job: return a string when OpenAI asks.&lt;/p&gt;

&lt;h3&gt;
  
  
  The drift I caught on myself
&lt;/h3&gt;

&lt;p&gt;Then I found something that actually was our mistake.&lt;/p&gt;

&lt;p&gt;Our public &lt;a href="https://github.com/publora-team/mcp-server" rel="noopener noreferrer"&gt;&lt;code&gt;mcp-server&lt;/code&gt; repo&lt;/a&gt; defines 18 tools, four of them for LinkedIn analytics: post stats, account stats, followers and profile summary. The live server exposes 16, and none of those analytics tools are among them. They exist in the REST API, but aren't exposed through MCP.&lt;/p&gt;

&lt;p&gt;The public repo had fallen behind the live server, and the app description was based on the repo. So we were describing analytics that someone using MCP couldn't actually access. I removed them from the description and release notes.&lt;/p&gt;

&lt;p&gt;Then I found the same old description in two more places: the README for the Zed extension and the PR for the Docker MCP Catalog. The Zed PR was also pointing at a commit from before the correction, so merging it would have put the outdated claim into another catalog. We'd updated the product and missed a few places where we'd described it. Easy enough to do when those descriptions live in different repos and submissions.&lt;/p&gt;

&lt;h3&gt;
  
  
  The rejection that wasn't about the code
&lt;/h3&gt;

&lt;p&gt;Submitted August 5, every wizard step completed in one pass. Rejected August 24.&lt;/p&gt;

&lt;p&gt;The message was: "We're unable to complete your sign-in or OAuth flow… ensure valid, working credentials… no additional setup or verification."&lt;/p&gt;

&lt;p&gt;Before changing anything, I walked through the flow again. Dynamic client registration returned 201. &lt;code&gt;/authorize&lt;/code&gt; sent me to our consent screen. The sign-in page in a clean browser was a normal email-and-password form with no captcha. &lt;code&gt;tools/list&lt;/code&gt; without a key returned the tools. API and MCP access were enabled on every plan, including free. Everything worked.&lt;/p&gt;

&lt;p&gt;Then we looked at the account we'd given the reviewer.&lt;/p&gt;

&lt;p&gt;Our own accounts use Google sign-in. A reviewer can't use our Google account, and even if they tried, the second factor would land on our phone. We'd managed to give them credentials for an account they had no way to get into.&lt;/p&gt;

&lt;p&gt;Even worse, Canva had rejected us for the same reason before. Twice was enough to stop calling it bad luck. We simply didn't have "can a stranger actually use these credentials?" on our submission checklist.&lt;/p&gt;

&lt;h3&gt;
  
  
  What actually fixed it
&lt;/h3&gt;

&lt;p&gt;We created a separate reviewer account: email and password, email confirmed ahead of time, a couple of channels connected, with some posts and drafts already there. We also included eight test cases — five positive and three negative.&lt;/p&gt;

&lt;p&gt;The most useful one was a full write-path test through &lt;code&gt;publora-playground&lt;/code&gt;. It's a fake publishing target: it validates the post against the same rules as a real one and returns the same kind of response, but throws the request away at the end instead of publishing it anywhere. A reviewer can go through the whole publishing flow without putting a test post on someone's actual timeline.&lt;/p&gt;

&lt;p&gt;It took about thirty lines and ended up being one of the most useful things we added for the submission. The reviewer could finally test writes without needing a disposable social account.&lt;/p&gt;

&lt;p&gt;We resubmitted on the evening of August 24. &lt;a href="https://chatgpt.com/plugins/plugin_asdk_app_6a7074e91c008191942479b885fb0d9b" rel="noopener noreferrer"&gt;Approved September 4.&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What I wrote down for next time
&lt;/h3&gt;

&lt;p&gt;The main addition to my submission checklist is pretty basic: go through the product as the reviewer, not as yourself.&lt;/p&gt;

&lt;p&gt;The server worked in Claude and Cursor. It followed the spec. OAuth worked when we tested it. But ChatGPT asked for the tool list in a different order, its scanner expected metadata we hadn't needed elsewhere, and the reviewer needed a login that didn't depend on being us. Those were the things that took the month.&lt;/p&gt;




&lt;p&gt;I did this with Claude — it wrote the code and configs, while I checked what was happening and decided what needed changing. The funny part is that almost none of those decisions involved changing the server itself. Most of the work ended up being metadata, descriptions and access.&lt;/p&gt;

&lt;p&gt;What's gotten your product bounced from a platform review — something actually broken, or something you never thought a reviewer would run into?&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>api</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I sell social media automation. My survey says people want the opposite.</title>
      <dc:creator>Eugeniya Ivanova</dc:creator>
      <pubDate>Thu, 03 Sep 2026 08:07:39 +0000</pubDate>
      <link>https://dev.to/eugeniya_ivanova_4a58eadc/i-sell-social-media-automation-my-survey-says-people-want-the-opposite-13e1</link>
      <guid>https://dev.to/eugeniya_ivanova_4a58eadc/i-sell-social-media-automation-my-survey-says-people-want-the-opposite-13e1</guid>
      <description>&lt;p&gt;I work at &lt;a href="https://publora.com" rel="noopener noreferrer"&gt;Publora&lt;/a&gt; — an API and MCP for publishing to social networks from one place, including through an AI agent. So, in a sense, my job is selling automation for posting. Which made the answers I started getting from actual people a little awkward.&lt;/p&gt;

&lt;p&gt;I've been doing a small study — nothing fancy, just messaging people directly and asking: what do you use to post, and why do you do it that way? No pitch, no setup. It's an ongoing thing, reaching out to people across different niches one by one, so this isn't a clean statistic — more a pile of honest answers I'm still adding to. A few hundred replies in, and I'm still going.&lt;/p&gt;

&lt;p&gt;The answers are all over the place, but one thing keeps coming up. People aren't asking for more automation. Quite a lot of them want to keep the final step for themselves.&lt;/p&gt;

&lt;p&gt;A few answers, anonymized.&lt;/p&gt;

&lt;p&gt;One person told me: "I use AI rarely, do the core work myself. Sometimes I go into ChatGPT or Gemini as a 'second brain' — when I'm out of ideas or a deadline's closing in. But at the end of the day it's still just a machine."&lt;/p&gt;

&lt;p&gt;So AI is useful to think with. That doesn't mean they want it doing everything.&lt;/p&gt;

&lt;p&gt;Another: "I use Claude, but not for posting — it sort of sets me up, and I publish myself."&lt;/p&gt;

&lt;p&gt;And a third was very direct: "I'm wary of AI tools having access to my account. LinkedIn isn't forgiving about it."&lt;/p&gt;

&lt;p&gt;That one wasn't really about whether AI-written posts sound authentic. They simply didn't want another tool having that much access to their account.&lt;/p&gt;

&lt;p&gt;Then there was this: "I like hitting publish myself. In a world where everything's polished, it makes the content feel a little more alive."&lt;/p&gt;

&lt;p&gt;Different reasons: sometimes security, sometimes control, sometimes just liking the fact that there's still a person involved. But I kept hearing versions of the same thing. Draft for me, help me think, prepare it — fine. I'll publish it.&lt;/p&gt;

&lt;p&gt;Nobody was dreaming about an agent happily posting all day while they went for coffee.&lt;/p&gt;

&lt;p&gt;At first this was slightly uncomfortable to hear, considering what I do for a living. But the more replies I read, the less it sounded like an objection to automation. It sounded more like people being quite specific about which parts they actually want automated.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where that caution stops being a quirk
&lt;/h3&gt;

&lt;p&gt;One answer stood out because it came from someone managing several brands rather than just their own account.&lt;/p&gt;

&lt;p&gt;They prefer tools with integrations because content and reporting are easier there than doing everything natively. The problem is that not every platform has the integrations they need, and the more channels a brand uses, the messier the whole setup gets.&lt;/p&gt;

&lt;p&gt;That changes the problem quite a bit.&lt;/p&gt;

&lt;p&gt;Being cautious about giving AI access to your own LinkedIn account is one thing. Being an agency responsible for ten clients' accounts is another. Now you're dealing with reputations that aren't yours, client credentials, several calendars, approvals living somewhere in email, and the possibility of someone putting one client's post on another client's account.&lt;/p&gt;

&lt;p&gt;At that point, automating everything sounds terrifying. Doing everything manually isn't much better. Ten clients still have to fit into the same working day.&lt;/p&gt;

&lt;p&gt;So you need something in between.&lt;/p&gt;

&lt;h3&gt;
  
  
  What that looks like in practice
&lt;/h3&gt;

&lt;p&gt;After reading the replies, I kept coming back to three things.&lt;/p&gt;

&lt;p&gt;The first is keeping clients separate. Each one needs their own connections, content and calendar, without the possibility of a post quietly ending up on somebody else's account. A posting mistake on your own account is embarrassing. Doing it to a client's account can get expensive.&lt;/p&gt;

&lt;p&gt;The second is passwords. Ideally, an agency shouldn't have them at all. The client connects their account through an OAuth link, and the team can work with it without ever knowing the login. The person who told me they were wary of tools having access to their account wasn't being paranoid. That's a perfectly reasonable thing to worry about.&lt;/p&gt;

&lt;p&gt;And then there's approval. An agent can draft a post. It can prepare it, schedule it, do the repetitive bits. But publishing can still wait for a person to approve it. In a team, that can also mean the person who wrote the post isn't the person who approves it.&lt;/p&gt;

&lt;p&gt;It's basically the same habit I kept seeing in the survey, only with more people involved.&lt;/p&gt;

&lt;p&gt;We build this at Publora — it's our &lt;a href="https://agencies.publora.com" rel="noopener noreferrer"&gt;workspace for agencies&lt;/a&gt;, and the API is &lt;a href="https://docs.publora.com" rel="noopener noreferrer"&gt;documented here&lt;/a&gt;. I won't turn this into a tour of the product; if that's what you're looking for, the links are there.&lt;/p&gt;

&lt;p&gt;What's been more interesting for me is realizing that some of the things we'd been thinking about as product features map almost exactly to things people told me they were worried about.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I took from it
&lt;/h3&gt;

&lt;p&gt;Automation is usually sold as: let the machine do this for you.&lt;/p&gt;

&lt;p&gt;But after reading these replies, I think there's a pretty big group of people asking for something slightly different: help me do it, just don't take the whole thing away from me.&lt;/p&gt;

&lt;p&gt;For someone posting to their own account, that might simply mean wanting to hit Publish themselves. For an agency, the same instinct turns into much more practical requirements: keep clients separate, don't pass passwords around, and make sure a person approves what goes out.&lt;/p&gt;

&lt;p&gt;I find it slightly funny that it took working inside an automation company to make me think harder about what shouldn't be automated.&lt;/p&gt;




&lt;p&gt;And for the record, I wrote this with AI. It helped me sort through material and clean up the draft. Deciding what was interesting in a few hundred replies was still my job.&lt;/p&gt;

&lt;p&gt;I'm keeping that bit.&lt;/p&gt;

&lt;p&gt;Where do you draw the line? What are you happy to hand to an agent, and what do you still want to do yourself?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>automation</category>
      <category>socialmedia</category>
      <category>discuss</category>
    </item>
    <item>
      <title>I published our app on Zapier. The no-code platform made me write code.</title>
      <dc:creator>Eugeniya Ivanova</dc:creator>
      <pubDate>Mon, 31 Aug 2026 06:29:04 +0000</pubDate>
      <link>https://dev.to/eugeniya_ivanova_4a58eadc/i-published-our-app-on-zapier-the-no-code-platform-made-me-write-code-2hc3</link>
      <guid>https://dev.to/eugeniya_ivanova_4a58eadc/i-published-our-app-on-zapier-the-no-code-platform-made-me-write-code-2hc3</guid>
      <description>&lt;p&gt;Publora is in the Zapier app directory now. I didn't do it to tick a box on some distribution list. My job is making our product easy to live with, and if a user has an agent that can wire us in deeper so they don't have to build the plumbing themselves, I'll go make that happen. Zapier is exactly that case: it connects Publora to thousands of other apps, so nobody has to hand-roll the integration.&lt;/p&gt;

&lt;p&gt;Worth it. I'd just add that "a no-code platform" and "publishing your own app on a no-code platform" turn out to be two very different Zapiers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prove it works for users who don't exist yet
&lt;/h3&gt;

&lt;p&gt;Here's the requirement I reread three times, sure I'd misunderstood.&lt;/p&gt;

&lt;p&gt;To submit an app for review, every trigger, every action, and every search has to be tested inside a live Zap, turned on, with at least one successful run in the history. You can't delete those Zaps; the reviewer can ask to see them.&lt;/p&gt;

&lt;p&gt;So the logic goes like this. You want to publish an app so people can start using it. But to publish it, you first have to prove it's already being used. Run every component for real, as if you had the users you're publishing it to attract. The app isn't in the directory yet, and a history of real use already has to exist.&lt;/p&gt;

&lt;p&gt;You end up standing in for your own users who aren't there yet. You build the Zaps, run each one, make sure every one has a green run, and don't touch them afterward.&lt;/p&gt;

&lt;h3&gt;
  
  
  A routine task you run like a rocket launch
&lt;/h3&gt;

&lt;p&gt;The second surprise. My tasks here are the plain ones: schedule a post, publish a post, delete a post. This isn't a satellite launch. It's what our API does a thousand times a day over one line of code.&lt;/p&gt;

&lt;p&gt;As a Zapier app, each of those ordinary tasks has to be wrapped, configured, and run live on its own. Create Post, Update Post, Delete Post, two triggers, two searches, each with its own test run under the validator's eye. Scheduling a post is something I can describe in one sentence. Here it became a component with a run history.&lt;/p&gt;

&lt;p&gt;Then the small surprises a "no-code" promise doesn't quite imply. The connection label, the line that tells the user which account they connected, couldn't just read a field. &lt;code&gt;{{connections.0.username}}&lt;/code&gt; doesn't work, because Zapier won't follow a path into an array. So the label needs actual JavaScript:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;connections&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;bundle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;inputData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;connections&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;first&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;connections&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;first&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Publora&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's in a box labeled "label." On a no-code platform.&lt;/p&gt;

&lt;p&gt;The triggers are the other place the word stretches. These aren't polling. They're REST Hooks on our own webhooks, which means subscribing and unsubscribing are API calls you define by hand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// subscribe&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.publora.com/api/v1/webhooks&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Accept&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-publora-key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bundle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Zapier - Post Published&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bundle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;targetUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;events&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;post.published&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;throwForStatus&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webhook&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What subscribe returns is not incidental. Zapier hands it back to you as &lt;code&gt;bundle.subscribeData&lt;/code&gt;, and unsubscribe reads the webhook id out of it to call &lt;code&gt;DELETE /webhooks/{id}&lt;/code&gt;. Return the wrong shape and the Zap turns on cleanly, then never cleans up after itself.&lt;/p&gt;

&lt;p&gt;A few more edges, for anyone about to do this. A field's type and key are locked after you create it, and if you get the shape of a step wrong and you don't fix it, you recreate it from scratch. The first published version gets locked for editing once real people are on it, so you clone it into a new one and make changes there. And the editor session expires with a cheerful &lt;code&gt;failed csrf&lt;/code&gt;; you reload, and the code you just wrote is gone.&lt;/p&gt;

&lt;p&gt;None of it is fatal. I just walked in with a one-line task and walked out with versioning, user migrations, and a chunk of JavaScript in a field called "label."&lt;/p&gt;

&lt;h3&gt;
  
  
  Where it landed
&lt;/h3&gt;

&lt;p&gt;The app passed review after eight rounds of reviewer notes, then out into the directory with a Beta tag. Zapier's beta runs ninety days, and you leave it one of two ways: pull in enough active users, or embed their widget inside your own product. Familiar shape. The platform holds you at "not quite yet," and the key out is either bring it an audience or let it inside your product.&lt;/p&gt;

&lt;p&gt;I don't regret the trip. The directory listing genuinely lowers the effort for anyone who wants to post from whatever workflow they already live in, and I wrote them a separate tutorial for that. But the "no-code" part I left somewhere around the halfway mark. For the user assembling a Zap out of ready-made blocks, sure, no code. For the person making those blocks, no-code ends exactly where anything past a single step begins.&lt;/p&gt;




&lt;p&gt;For the record, I'm not an engineer. My job is getting people to use our product, and I built this with Claude: it wrote the code, I led, tested, and sent it back to redo. Which is maybe the honest shape of "no-code" in 2026 anyway. The code didn't disappear. It just moved to someone else, or something else.&lt;/p&gt;

&lt;p&gt;Did your no-code platforms keep the promise when you went past a single step? Or did you end up opening the code editor too?&lt;/p&gt;

</description>
      <category>zapier</category>
      <category>nocode</category>
      <category>api</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The agent posted successfully. To the wrong account.</title>
      <dc:creator>Eugeniya Ivanova</dc:creator>
      <pubDate>Thu, 27 Aug 2026 08:31:54 +0000</pubDate>
      <link>https://dev.to/eugeniya_ivanova_4a58eadc/the-agent-posted-successfully-to-the-wrong-account-3kf3</link>
      <guid>https://dev.to/eugeniya_ivanova_4a58eadc/the-agent-posted-successfully-to-the-wrong-account-3kf3</guid>
      <description>&lt;p&gt;Back in July I wrote about &lt;a href="https://dev.to/eugeniya_ivanova_4a58eadc/why-every-ai-agent-eventually-fights-social-media-apis-35e0"&gt;what it takes to wire an AI agent into social platforms&lt;/a&gt;: six OAuth flows, three-step media uploads, tokens expiring on their own private schedules. The conclusion was to hide all of it behind a single tool call and stop looking at it.&lt;/p&gt;

&lt;p&gt;Two months on, that part is done. Our MCP server answers over OAuth now, with proper metadata at &lt;code&gt;/.well-known/oauth-authorization-server&lt;/code&gt;, PKCE, and dynamic client registration, so connecting an editor no longer involves typing a key into a config file. Sixteen tools, one endpoint. The plumbing works.&lt;/p&gt;

&lt;p&gt;What I got wrong was assuming the plumbing was the risky part.&lt;/p&gt;

&lt;p&gt;My job is getting our product into people's hands, which means I use it the way I hope other people will: I ask an agent to publish and go back to what I was doing. Living with that for a couple of months taught me that once an agent has write access, failures stop announcing themselves.&lt;/p&gt;

&lt;h3&gt;
  
  
  An identifier that looks exactly like an identifier
&lt;/h3&gt;

&lt;p&gt;A language model produces plausible-looking strings. That is the entire skill. Ask it to post to LinkedIn and it can hand the API a value with the right prefix, the right length, the right shape, and the wrong account.&lt;/p&gt;

&lt;p&gt;Nothing about that request is malformed. There is no error to catch. The API was asked to do something specific and it did it.&lt;/p&gt;

&lt;p&gt;So the first instruction our server gives any client is not a description of what it does. It's a rule: call &lt;code&gt;list_connections&lt;/code&gt; first, copy each &lt;code&gt;platformId&lt;/code&gt; verbatim, never invent one. Before the tool list, before the examples, before anything explaining what the product is for.&lt;/p&gt;

&lt;p&gt;Writing documentation for a reader who will confidently improvise is a genuinely different job from writing it for a human who will get bored and skim.&lt;/p&gt;

&lt;h3&gt;
  
  
  "Tomorrow at 9am" is a timezone question
&lt;/h3&gt;

&lt;p&gt;The API takes ISO 8601 in UTC and nothing else. So when you say "tomorrow at 9am," something has to decide which 9am you meant, and that something is the model.&lt;/p&gt;

&lt;p&gt;It's right most of the time. When it isn't, nothing surfaces at the call. The response is a normal success, the post sits in the queue with a perfectly valid timestamp, and you find out at 4am from the post itself.&lt;/p&gt;

&lt;p&gt;I now read the scheduled time back in the confirmation. Not because the model is bad at arithmetic, but because a wrong answer here is indistinguishable from a right one until it's too late to matter.&lt;/p&gt;

&lt;h3&gt;
  
  
  Accepted now, dead later
&lt;/h3&gt;

&lt;p&gt;Instagram, TikTok and YouTube won't publish without media. Nothing stops an agent from scheduling a text-only Instagram post: it validates, it enters the queue, it sits there looking healthy for a day, and it dies at publish time.&lt;/p&gt;

&lt;p&gt;Queue membership is not a promise. It's the kind of distinction you only learn by getting burned, because until then the failure is completely invisible.&lt;/p&gt;

&lt;h3&gt;
  
  
  The annotations nobody looks at
&lt;/h3&gt;

&lt;p&gt;MCP lets a server tag each tool with hints about what it does: &lt;code&gt;readOnlyHint&lt;/code&gt;, &lt;code&gt;destructiveHint&lt;/code&gt;. Ours are filled in. Sixteen tools, six of them flagged destructive: deleting posts, deleting media, removing a LinkedIn comment or reaction.&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;"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;"delete_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;"annotations"&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;"readOnlyHint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"destructiveHint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&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;They're advisory. A client can ignore them entirely, and plenty do. But they cost almost nothing to add, and they're the only way a server can tell a client "this one deserves a confirmation dialog" without inventing a private protocol. If you run an MCP server and haven't filled them in, that's twenty minutes of work that lets every well-behaved client protect your users for you.&lt;/p&gt;

&lt;h3&gt;
  
  
  A fake platform to post into
&lt;/h3&gt;

&lt;p&gt;The fix I like most is the least clever one:&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;"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;"test"&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="s2"&gt;"publora-playground"&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;It accepts the post, validates it against the real rules, returns a normal response, and throws it away. Nothing reaches a real network.&lt;/p&gt;

&lt;p&gt;It exists because there was previously no honest way to answer "is this connected and working?" Every genuine end-to-end test involved putting something real on someone's real timeline, which is a fine way to test at 2am and an awful one at any other hour. Now the whole round trip is testable without an audience.&lt;/p&gt;

&lt;p&gt;Every integration that writes somewhere public should have one of these, and most don't.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I'd tell July
&lt;/h3&gt;

&lt;p&gt;I'm biased about the product, so here's the part that isn't about it.&lt;/p&gt;

&lt;p&gt;When you give an agent write access to anything outward-facing, the failure worth designing against is not the 500. Exceptions land in logs and somebody eventually reads them. The dangerous one is the call that succeeds and quietly does the wrong thing: right shape, wrong target, no error anywhere in the chain.&lt;/p&gt;

&lt;p&gt;The July version of this was "hide the complexity behind one tool call." I still think that's right. I'd just add the second half now: and make the tool call hard to get subtly wrong, because subtly wrong is the only kind of wrong that gets published.&lt;/p&gt;

&lt;p&gt;Posting from the terminal saved me a context switch. The guardrails are what made me willing to leave it running while I did something else.&lt;/p&gt;




&lt;p&gt;I drafted this with Claude and then checked every claim against the live server before publishing. The playground response, the tool annotations, and the OAuth metadata are all things I re-ran rather than remembered.&lt;/p&gt;

&lt;p&gt;If you run an agent with write access to production, where's your line: a dry-run target, tool annotations, or a human confirming every call?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>api</category>
      <category>mcp</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I built plugins for three editors. Everywhere, you're a guest in someone else's house</title>
      <dc:creator>Eugeniya Ivanova</dc:creator>
      <pubDate>Wed, 26 Aug 2026 06:34:45 +0000</pubDate>
      <link>https://dev.to/eugeniya_ivanova_4a58eadc/i-built-plugins-for-three-editors-everywhere-youre-a-guest-in-someone-elses-house-34ck</link>
      <guid>https://dev.to/eugeniya_ivanova_4a58eadc/i-built-plugins-for-three-editors-everywhere-youre-a-guest-in-someone-elses-house-34ck</guid>
      <description>&lt;p&gt;Over the last while I've built integrations for three places where people work with text and images: &lt;a href="https://dev.to/eugeniya_ivanova_4a58eadc/i-shipped-a-plugin-with-one-branch-i-hadnt-tested-so-i-built-a-fallback-lc8"&gt;Obsidian&lt;/a&gt;, VS Code, and Figma. Doing a few of them back to back, I noticed something you don't see from a single one.&lt;/p&gt;

&lt;p&gt;They're all desktop apps. For your integration to exist at all, the person first installs a program on their machine, and then, inside it, your plugin. You're not writing for the web. You're writing code locked inside someone else's app — and each app has its own runtime, its own rules, and its own wall for you to walk into. The web trained us to think an HTTP request is one line. Inside someone else's sandbox, it turns out even that has to be earned.&lt;/p&gt;

&lt;p&gt;Figma was the strictest host of the three. I'll tell it through Figma, because it's locked down tighter than Obsidian or VS Code, and everything shows up on it at once.&lt;/p&gt;

&lt;p&gt;The task was almost comically simple: select a frame, write a caption, pick your social accounts, publish — without exporting the image and opening a second app. We already had the publishing API, so I expected the Figma side to be small. And it was: the main plugin file is 120 lines. The work wasn't in them. It was around them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Figma gives you bytes, not a file
&lt;/h3&gt;

&lt;p&gt;The first version came together easily. When the selection changes, the plugin checks whether there's one exportable node and tells the UI what it found. For the preview it exports a small copy; for publishing, separately, at 2×.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;exportAsync&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PNG&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;constraint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SCALE&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;2× because the image still has a journey ahead of it: social networks recompress what you upload, and small text on a design goes noticeably softer by the time it lands in a feed.&lt;/p&gt;

&lt;p&gt;Then the first quirk of the foreign house. Figma hands the plugin not a file but raw PNG bytes — &lt;a href="https://www.figma.com/plugin-docs/api/properties/nodes-exportasync/" rel="noopener noreferrer"&gt;&lt;code&gt;exportAsync()&lt;/code&gt;&lt;/a&gt; returns a &lt;code&gt;Uint8Array&lt;/code&gt;. Our normal API won't eat that — it doesn't take a giant image stuffed into a JSON body. It creates a post first, hands the client a temporary upload URL, waits for the file to reach storage, and only then attaches the media to the post.&lt;/p&gt;

&lt;p&gt;So "publish one frame with a caption" turned into this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;create post → get upload URL → PUT PNG → complete media → update post
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a post with no image, it's one call. With an image, the post lives as a draft while the upload happens, and moves to its final state only once the media is there. That order saves you from a particularly annoying failure: scheduling a post, feeling pleased, then learning the image never uploaded.&lt;/p&gt;

&lt;h3&gt;
  
  
  Two worlds inside one plugin
&lt;/h3&gt;

&lt;p&gt;A Figma plugin isn't one JavaScript. &lt;a href="https://www.figma.com/plugin-docs/how-plugins-run/" rel="noopener noreferrer"&gt;The main thread&lt;/a&gt; sees the document — that's where we read the selection and call &lt;code&gt;exportAsync()&lt;/code&gt;. Network calls live in the UI, a separate context. Between them, a wall.&lt;/p&gt;

&lt;p&gt;So the exported &lt;code&gt;Uint8Array&lt;/code&gt; has to be thrown from the main thread over to the panel before anything can upload it. Convert it to a plain array, send it with &lt;code&gt;postMessage&lt;/code&gt;, reassemble the bytes on the other side.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// main thread&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;exportSelection&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;figma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postMessage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;exported&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// UI&lt;/span&gt;
&lt;span class="nx"&gt;exportedBytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not hard. Just one of those details that vanishes without a trace when you picture the feature in your head as "export the frame and send it to the API."&lt;/p&gt;

&lt;p&gt;The next detail didn't let go so easily. The plugin UI runs in a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Origin" rel="noopener noreferrer"&gt;sandboxed iframe&lt;/a&gt; with &lt;code&gt;Origin: null&lt;/code&gt;. Our API, naturally, wasn't set up to accept browser requests from &lt;code&gt;null&lt;/code&gt; — so the code worked right up to the point where it was supposed to do something useful. The classic: all green until it reaches the part that matters.&lt;/p&gt;

&lt;p&gt;I ended up putting a small Cloudflare Worker between the plugin and the API. It forwards requests and adds the CORS headers Figma insists on. It doesn't store the key or add a second auth system — the key sits in &lt;a href="https://www.figma.com/plugin-docs/api/figma-clientStorage/" rel="noopener noreferrer"&gt;&lt;code&gt;figma.clientStorage&lt;/code&gt;&lt;/a&gt; and just gets passed through to Publora when the plugin makes a request.&lt;/p&gt;

&lt;p&gt;We already had a similar worker for our Canva integration, but I made a separate one for Figma. Canva was on review at the time, and shipping new routes into infrastructure a reviewer might be poking at right then was a bad trade for saving one tiny worker.&lt;/p&gt;

&lt;h3&gt;
  
  
  A composer stops being simple very fast
&lt;/h3&gt;

&lt;p&gt;Once the requests finally worked, the boring per-platform rules came back.&lt;/p&gt;

&lt;p&gt;The plugin pulls the accounts already connected to Publora and shows them with usernames and avatars. You can pick several — which immediately raises the question of which limit the character counter should count against.&lt;/p&gt;

&lt;p&gt;LinkedIn allows far more text than X. If both are checked, showing the LinkedIn limit is useless when X is going to reject the post anyway. So the counter uses the strictest limit among the selected networks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;captionLimit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;limits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;selected&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;CAPTION_LIMITS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;platformOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;limits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;limits&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Media works the same way. Publora won't take a text-only post for Instagram, TikTok, or YouTube. If one of those is selected and there isn't exactly one frame selected in Figma, the plugin says so and disables Publish instead of waiting for the API to object.&lt;/p&gt;

&lt;p&gt;There are a handful of small cases like that in the UI. Two selected frames don't make two posts; the plugin asks you to pick one. A custom schedule time isn't valid until you've chosen a date. After a successful send, the form is replaced with a confirmation rather than leaving an active Publish button sitting there, a double post one impatient click away.&lt;/p&gt;

&lt;p&gt;Nothing impressive in that code. It just took more time than getting &lt;code&gt;exportAsync()&lt;/code&gt; to work.&lt;/p&gt;

&lt;h3&gt;
  
  
  And then you have to submit it
&lt;/h3&gt;

&lt;p&gt;The other half of "make a Figma plugin" is making something Figma will actually list.&lt;/p&gt;

&lt;p&gt;Because ours talks to an external service, the Community description says it outright: you need a Publora account, there's a free plan, the key is stored on the user's machine. Privacy policy and support contact are right there too.&lt;/p&gt;

&lt;p&gt;The manifest has to be specific about &lt;a href="https://www.figma.com/plugin-docs/manifest/#networkaccess" rel="noopener noreferrer"&gt;network access&lt;/a&gt;. Ours lists the worker, Publora's media domain, and the storage hosts that receive the exported PNG. No wildcard for "whatever the upload API hands me next."&lt;/p&gt;

&lt;p&gt;And we kept the listing strictly about what the plugin does in Figma. No stray product features, no mention of MCP. Figma has its own rules about MCP access to files, and it isn't needed here anyway: the plugin takes a design out of Figma and sends it where the user chose.&lt;/p&gt;

&lt;p&gt;Before submitting, we ran the plugin in desktop Figma on a live Publora account. It connected, pulled the account list with its avatar, applied the LinkedIn limit, and created a real draft we confirmed through the API. The branch I trusted least was the image path — it crosses nearly every boundary in the plugin at once: Figma export, &lt;code&gt;postMessage&lt;/code&gt;, the UI, the worker, the API, storage. That's the one I watched hardest.&lt;/p&gt;

&lt;p&gt;It's on review now, so I don't have a tidy "and it was approved in six minutes" ending. Figma doesn't promise a fixed time either, so the last step of the build is glancing at the status occasionally and leaving it alone.&lt;/p&gt;

&lt;h3&gt;
  
  
  What it adds up to
&lt;/h3&gt;

&lt;p&gt;The funny part is that the Figma-specific code really was small. Those 120 lines do almost exactly what I pictured at the start. What turned the feature into a project was everything between "I have PNG bytes" and "this can safely become a social post."&lt;/p&gt;

&lt;p&gt;And it isn't only Figma. Obsidian made me go through its own &lt;a href="https://docs.obsidian.md/Reference/TypeScript+API/requestUrl" rel="noopener noreferrer"&gt;&lt;code&gt;requestUrl&lt;/code&gt;&lt;/a&gt; for network calls, because plain fetch hits the engine's limits on mobile. VS Code has its own ways. Figma has two contexts and &lt;code&gt;Origin: null&lt;/code&gt;. Every time, you take the same task — "send a post" — and rewrite it for a different sandbox, because you're a guest in someone else's app, not at home on the web.&lt;/p&gt;

&lt;p&gt;Whether it's worth it is a separate question. People work in these editors, and meeting them where they already are is more honest than dragging them over to you. But every one of those visits is a new runtime, a new wall, and a new worker to route around somebody's sandbox. I've done three, and I think I'm only starting to understand what I signed up for.&lt;/p&gt;




&lt;p&gt;I built this plugin with Claude — it wrote most of the code. The decisions, the testing, and what to do at each wall were mine.&lt;/p&gt;

&lt;p&gt;When your user lives inside someone else's desktop app, which way do you go: climb in there with them, or pull them out to the browser and into yours?&lt;/p&gt;

</description>
      <category>figma</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>plugins</category>
    </item>
    <item>
      <title>I shipped a plugin with one branch I hadn't tested — so I built a fallback</title>
      <dc:creator>Eugeniya Ivanova</dc:creator>
      <pubDate>Thu, 20 Aug 2026 08:30:58 +0000</pubDate>
      <link>https://dev.to/eugeniya_ivanova_4a58eadc/i-shipped-a-plugin-with-one-branch-i-hadnt-tested-so-i-built-a-fallback-lc8</link>
      <guid>https://dev.to/eugeniya_ivanova_4a58eadc/i-shipped-a-plugin-with-one-branch-i-hadnt-tested-so-i-built-a-fallback-lc8</guid>
      <description>&lt;p&gt;I wrote the Obsidian plugin with Claude Code: it typed the code; the rules, reviews, and decisions were mine. I led, argued with it, redid things, and submitted the plugin for review myself. I'm not an engineer. My job is getting people to actually use our product.&lt;/p&gt;

&lt;p&gt;The plugin itself is simple: one &lt;code&gt;main.js&lt;/code&gt;, 557 lines, zero dependencies, &lt;a href="https://docs.obsidian.md/Reference/TypeScript+API/Plugin" rel="noopener noreferrer"&gt;Obsidian API&lt;/a&gt; only. It takes the note you have open and sends it to your social accounts on a schedule.&lt;/p&gt;

&lt;p&gt;Most of the evening, though, went into getting it through the Obsidian catalog. Here's what I tripped over, so hopefully you don't have to.&lt;/p&gt;

&lt;h3&gt;
  
  
  Six fixes the Obsidian review asked for
&lt;/h3&gt;

&lt;p&gt;Before a plugin lands in the catalog, it gets reviewed against the &lt;a href="https://docs.obsidian.md/Plugins/Releasing/Plugin+guidelines" rel="noopener noreferrer"&gt;Plugin guidelines&lt;/a&gt;. I read them beforehand and still came back with notes. Some requirements are much easier to notice once a reviewer points directly at them.&lt;/p&gt;

&lt;p&gt;These were my six:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No &lt;code&gt;innerHTML&lt;/code&gt;.&lt;/strong&gt; Build the DOM with &lt;code&gt;createEl&lt;/code&gt;. They check for this because &lt;code&gt;innerHTML&lt;/code&gt; plus third-party text is an open door.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Styles go in &lt;code&gt;styles.css&lt;/code&gt;,&lt;/strong&gt; not inline in JS. Inline styles got bounced.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Modal titles use &lt;code&gt;titleEl&lt;/code&gt;,&lt;/strong&gt; not your own heading inside the modal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No top-level heading in settings,&lt;/strong&gt; and use sentence case, not Title Case Across Every Word.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drop the plugin name from command titles.&lt;/strong&gt; Obsidian adds the prefix itself; otherwise the command palette shows &lt;code&gt;Publora: Publora: Send note&lt;/code&gt;. It stutters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network requests go through &lt;a href="https://docs.obsidian.md/Reference/TypeScript+API/requestUrl" rel="noopener noreferrer"&gt;&lt;code&gt;requestUrl&lt;/code&gt;&lt;/a&gt;.&lt;/strong&gt; Plain &lt;code&gt;fetch&lt;/code&gt; runs into problems on mobile, so if the plugin is &lt;code&gt;isDesktopOnly: false&lt;/code&gt;, use Obsidian's API.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this changed what the plugin did. But doing it upfront would have saved me a review round and a couple of days.&lt;/p&gt;

&lt;h3&gt;
  
  
  A portal that won't explain its own errors
&lt;/h3&gt;

&lt;p&gt;Submission goes through &lt;a href="https://community.obsidian.md/" rel="noopener noreferrer"&gt;community.obsidian.md&lt;/a&gt;: sign in with your Obsidian account, then connect GitHub separately. The happy path is documented. The potholes aren't.&lt;/p&gt;

&lt;p&gt;Mine:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"You do not own this repository" — when you clearly have access.&lt;/strong&gt; If the repo belongs to an organization, choose the organization as the submission owner instead of &lt;code&gt;Myself&lt;/code&gt;. The error disappeared immediately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your GitHub organization membership has to be public.&lt;/strong&gt; The portal only sees public members. None of ours were public, so as far as the portal was concerned, our organization contained approximately nobody.&lt;/p&gt;

&lt;p&gt;One request fixes your membership:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PUT /orgs/{org}/public_members/{username}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The catch: you can only make your &lt;em&gt;own&lt;/em&gt; membership public this way. &lt;a href="https://docs.github.com/en/rest/orgs/members#set-public-organization-membership-for-the-authenticated-user" rel="noopener noreferrer"&gt;Each person has to do it with their own token.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The rate limiter.&lt;/strong&gt; After several attempts, the portal starts replying with &lt;code&gt;Please wait before trying again&lt;/code&gt;. It doesn't say how long, and clicking again only makes things worse.&lt;/p&gt;

&lt;p&gt;The solution turned out to be extremely technical: leave it alone and come back later. Then click once.&lt;/p&gt;

&lt;h3&gt;
  
  
  Proving the release files came from your code
&lt;/h3&gt;

&lt;p&gt;The review passed, but there was one note left: the release files had no artifact attestation.&lt;/p&gt;

&lt;p&gt;If you haven't run into this before, the problem is pretty straightforward. Users don't install the source code they're looking at in your GitHub repository. They install the built &lt;code&gt;main.js&lt;/code&gt; attached to a GitHub Release.&lt;/p&gt;

&lt;p&gt;Those two files &lt;em&gt;can&lt;/em&gt; be different.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.github.com/en/actions/security-for-github-actions/using-artifact-attestations/using-artifact-attestations-to-establish-provenance-for-builds" rel="noopener noreferrer"&gt;Artifact attestation&lt;/a&gt; lets someone verify where the release file came from. GitHub Actions, through &lt;a href="https://www.sigstore.dev/" rel="noopener noreferrer"&gt;Sigstore&lt;/a&gt;, ties the artifact to a specific commit and workflow. This is the &lt;code&gt;release.yml&lt;/code&gt; I ended up with — it also checks the tag matches the manifest version before it publishes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Release&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;*'&lt;/span&gt;

&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
  &lt;span class="na"&gt;id-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
  &lt;span class="na"&gt;attestations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;release&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Check the tag matches the manifest&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;tag="${GITHUB_REF_NAME}"&lt;/span&gt;
          &lt;span class="s"&gt;manifest="$(node -p "require('./manifest.json').version")"&lt;/span&gt;
          &lt;span class="s"&gt;if [ "$tag" != "$manifest" ]; then&lt;/span&gt;
            &lt;span class="s"&gt;echo "Tag $tag does not match manifest version $manifest"&lt;/span&gt;
            &lt;span class="s"&gt;exit 1&lt;/span&gt;
          &lt;span class="s"&gt;fi&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Attest the release assets&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/attest-build-provenance@v2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;subject-path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;main.js&lt;/span&gt;
            &lt;span class="s"&gt;manifest.json&lt;/span&gt;
            &lt;span class="s"&gt;styles.css&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Publish the release&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;GH_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.token }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;gh release create "${GITHUB_REF_NAME}" \&lt;/span&gt;
            &lt;span class="s"&gt;main.js manifest.json styles.css \&lt;/span&gt;
            &lt;span class="s"&gt;--title "${GITHUB_REF_NAME}" \&lt;/span&gt;
            &lt;span class="s"&gt;--generate-notes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The part that cost me the most time was these two permissions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;id-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
&lt;span class="na"&gt;attestations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without them, my workflow went green but no attestation appeared.&lt;/p&gt;

&lt;p&gt;Everything looked fine. Everything was not fine.&lt;/p&gt;

&lt;p&gt;Once I added the permissions, the check actually passed and &lt;code&gt;verified GitHub artifact attestation&lt;/code&gt; showed up in the review.&lt;/p&gt;

&lt;p&gt;This is probably my favorite part of the whole process: nobody has to take your word for it. The release itself carries proof of where it came from.&lt;/p&gt;

&lt;h3&gt;
  
  
  The branch I hadn't tested
&lt;/h3&gt;

&lt;p&gt;At submission time, I still had one thing I hadn't managed to verify.&lt;/p&gt;

&lt;p&gt;Login in the plugin goes through OAuth, so the user gets a token. Our REST API, meanwhile, had historically been used with an API key.&lt;/p&gt;

&lt;p&gt;Would the API accept the token from this new OAuth flow?&lt;/p&gt;

&lt;p&gt;Probably.&lt;/p&gt;

&lt;p&gt;Had I actually tested it?&lt;/p&gt;

&lt;p&gt;No.&lt;/p&gt;

&lt;p&gt;I could ship it and find out from the first person whose button stopped working. People do this more often than conference talks would have you believe.&lt;/p&gt;

&lt;p&gt;Instead, I built a fallback. The plugin sends the credential — whether that's the signed-in token or a pasted key — in the same &lt;code&gt;x-publora-key&lt;/code&gt; header. If the request comes back &lt;code&gt;401&lt;/code&gt; and the token was the one that failed, it retries once with the API key from settings instead of stranding the user mid-post. If there's no key to fall back to, it says so in plain words rather than leaving a dead button:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;callApi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;plugin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;credential&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;plugin&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;plugin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;credential&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;credential&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Not connected yet. Open Settings, then Publora, and press Connect.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;usedToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;plugin&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;plugin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;oauth&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nx"&gt;credential&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;plugin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;oauth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;accessToken&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;requestUrl&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;BASE_URL&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-publora-key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;credential&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;throw&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// The signed-in token was refused. If a key is also configured, use it&lt;/span&gt;
    &lt;span class="c1"&gt;// rather than stranding the user mid-post.&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;usedToken&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;callApi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;({},&lt;/span&gt; &lt;span class="nx"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;oauth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;usedToken&lt;/span&gt;
        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Publora refused the signed-in account. Reconnect in Settings → Publora, or paste an API key there under Advanced.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Publora rejected the API key. Check it in Settings → Publora.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The retry is just the same function calling itself with &lt;code&gt;oauth&lt;/code&gt; nulled out, so it falls through to the key. A week later, working on OAuth for another add-on, I finally got to test the real thing: the REST API accepts the token fine, and the fallback never fired.&lt;/p&gt;

&lt;p&gt;But I didn't know that when I submitted the plugin.&lt;/p&gt;

&lt;p&gt;That's the bit I want to keep from this whole exercise. Sometimes you have a branch you can't test before release. You don't have to pretend otherwise. If the failure mode is predictable and the fallback is cheap, you can put the uncertainty into the program instead of handing it to the user.&lt;/p&gt;

&lt;h3&gt;
  
  
  Numbers I actually trust
&lt;/h3&gt;

&lt;p&gt;As of August 19: 33 installs.&lt;/p&gt;

&lt;p&gt;Not a lot.&lt;/p&gt;

&lt;p&gt;But the version distribution shows most of those installs on the latest version, so people are updating rather than installing once and disappearing.&lt;/p&gt;

&lt;p&gt;For comparison, our extension on another marketplace shows 772 "downloads." That counter also eats mirrors and editor caches. At one point it jumped by 26 in half an hour when basically nobody knew the extension existed.&lt;/p&gt;

&lt;p&gt;So right now I'll take the 33.&lt;/p&gt;

&lt;p&gt;At least I know what they mean.&lt;/p&gt;




&lt;p&gt;I built this plugin with AI, and I'm not particularly interested in hiding that. Claude Code typed most of it. I decided what it should do, reviewed what it produced, dealt with the submission, and decided what to do when I couldn't verify something before release.&lt;/p&gt;

&lt;p&gt;The typing is increasingly the easy part.&lt;/p&gt;

&lt;p&gt;The annoying part is still noticing the thing you haven't tested.&lt;/p&gt;

&lt;p&gt;What do you do when you reach release with one branch still uncertain: ship it and wait for the bug report, or build the fallback first?&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>opensource</category>
      <category>obsidian</category>
      <category>discuss</category>
    </item>
    <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>
  </channel>
</rss>
