Bassam Ismail
Building Press
Building Press·Part 6 of 6
Engineering

Building Press, Part 6: The datacenter can't post, so the laptop does

7 min read

Automating social posting did not look like a boundary problem at first. The session that kept dying looked like a bug. I would paste a fresh LinkedIn cookie into the Studio, the status pill would flip to "active," and for about three minutes everything looked healthy. Then the next queued post would fail, the pill would flip to "expired," and I would paste another cookie. I did this enough times to convince myself LinkedIn had absurdly short sessions. It does not. The sessions were fine. The problem was where I was trying to use them.

That is the whole story of the last boundary in Press, and it is the messiest one, because it is the only boundary I do not own both sides of. LinkedIn and X own their side, and they actively fight automation. My thesis for this part is narrow: when you cannot control the other side of a boundary, stop trying to fake your way across it and instead move your code to the side where the credential is legitimate.

Automating social posting belongs on the residential side

Press runs its Studio on a datacenter IP. That is correct for almost everything it does: building the static blog, serving the dashboard, talking to Postgres. It is exactly wrong for posting to social platforms. From a datacenter IP, LinkedIn and X treat a login or a post attempt as a bot and challenge or reject it. There is no header I can set to talk my way out of that. The IP itself is the signal.

So the Studio never posts. It does the part it is allowed to do, which is decide and record, and it hands the actual posting to something that lives where posting is allowed. Concretely, the Studio maintains a queue, and a worker running on a residential connection drains it. The worker is the only component that ever touches LinkedIn or X.

The shape matters. The queue is a database table, so approving a post is just a row transition the Studio already knows how to do, and posting is a separate, asynchronous job. If the worker is offline, drafts sit in the queue and nothing breaks. When the worker comes back, it polls for due drafts, posts each through a real browser session, captures the resulting URL, and writes that URL back onto the row so the Studio can show "posted" with a link. The Studio's job ends at "approved and queued." The worker's job is everything that requires a residential IP and a real session.

My first design tried to cheat automating social posting. I would log into LinkedIn in my normal browser, copy the session cookie, and paste it into the Studio, which handed it to the worker's headless browser. The reasoning felt sound: a valid cookie is a valid cookie. It was not sound, and the failure was fast and consistent.

Warning

A platform session minted in your normal browser will not survive being transplanted into an automation browser. LinkedIn binds the session to the original device fingerprint and challenges the replay, which is why pasted cookies "expire" within minutes. The status is honest; the cookie is not actually expired, it is being rejected.

The tell was that the cookie worked for a few minutes and then stopped. A genuinely expired credential fails immediately. A credential that works briefly and then dies is being re-evaluated by the platform and found suspicious. LinkedIn was watching the session move from the device that created it into a Playwright-driven browser on a different machine, and it was challenging the replay.

Deep-dive: the cookie-transplant trap, and the durable fix

The li_at cookie is not a portable bearer token. It is bound to the browser and device context it was created in. When the worker's headless Chromium replayed it, the request carried a different fingerprint than the one the cookie was issued against, and LinkedIn's anti-automation layer flagged it. The "active for three minutes, then expired" pattern was that re-evaluation happening on a slight delay, not a TTL.

The durable fix is to stop transplanting anything. Instead of moving a credential to where the code runs, the worker runs a one-time native login inside its own persistent browser profile, so the session is born where it is used. After that, the cookie and the fingerprint match on every request, because they came from the same browser. The session stops being suspicious because it stops being a transplant.

One incident made the cost of getting this wrong visible. A draft got stuck in the queue while the LinkedIn session was unauthenticated. The worker, polling for due drafts, re-opened LinkedIn every two minutes to try again. Each attempt churned the session further and gave LinkedIn more reason to distrust the account. The fix was to make the worker back off a channel it cannot authenticate rather than hammer it: if a channel is not logged in, stop trying that channel and surface the state, instead of retrying into a wall.

What I rejected, and what it still cannot do

I rejected posting from the datacenter IP directly. There was no version of that which worked; the IP is the disqualifier, and no amount of session hygiene changes it. I also rejected the cookie transplant, which is the more tempting dead end because it looks like it should work and even appears to work for a moment.

I want to be precise about what this design does not solve, because this boundary is the one where I have the least control and the most rough edges.

The worker can post, but it cannot reliably delete. Takedown depends on the platform's UI staying stable for an automated session, and that is far less dependable than posting, so removing something is partly a manual operation. I would rather state that plainly than pretend delete is one click.

The worker only captures a post's URL for some platforms. Where the platform exposes the resulting URL cleanly after posting, the Studio gets a real link back; where it does not, the row is marked posted without a canonical URL, and I reconcile that by hand if I need the link.

There is also a reach cost I accept on purpose. LinkedIn de-prioritizes posts that carry an external link in the body, so putting the canonical post URL inline trades distribution for attribution. The known mitigation is to put the link in the first comment instead of the body, which I have not built yet. For now the link goes in the body, and I take the reach hit knowingly rather than drop attribution.

The durable lesson

The thing I kept relearning, cookie after cookie, is that a credential is not a value you can carry around. It is a claim about a context, and platforms increasingly check the context, not just the value. The session was never the problem. My attempt to use it somewhere it was not issued was the problem.

That is the same move the rest of Press is built on, applied to the one boundary I share with someone hostile to automation. As in Building Press, Part 2: The data model is the spine, the system works because ownership is explicit. The database owns truth and the site only reads it. The revision owns the body and the post points at it. Here, the residential worker owns the session, because that is the only place the session is legitimate, and the Studio is not allowed to reach across that line and borrow it. Find the boundary, decide who is actually allowed to stand on each side, and build the system so neither side has to pretend to be the other.