Adversarial internecting

Last week, news broke that Netflix is attempting to purchase Letterboxd. This was a hot topic of discussion on every social site1, but Bluesky featured a sub-theme you won't find anywhere else: why don't we just rebuild this here?

View post on Bluesky

View post on Bluesky

View post on Bluesky

The SaaS ecosystem is just starting to feel the pain from AI agents that can quickly clone websites and apps. Most of the managed services that do this don't work so well yet, but at this point any decent software engineer probably could whip up a serviceable Letterboxd clone in a weekend. Some VCs already consider this an existential threat to certain business models and are making funding decisions with that in mind.

Of course, getting people to use your cloned app isn't that easy:

View post on Bluesky

That's why it's past time to talk about adversarial internecting.

View post on Bluesky

The internect is a series of tubes

If you aren't a Bluesky elder or otherwise deep in the ATProto developer community, you probably haven't heard the word "internect" before. The necessary context is that it isn't a thing:

View post on Bluesky

View post on Bluesky

There is no technical definition for what internecting means, really. Essentially it's just shorthand for "federated but on the Atmosphere specifically": connected to the overall ATProto network. That includes a variety of specific patterns and technologies depending on what is being connected to where.

But I think the social component is more interesting. Obviously we do not need a new word for this, so it's just a meme. And it's one you'll hear most from people who have been on Bluesky a long time, or who are actively building Atmosphere apps of their own. Typically, they have a maximalist view of what's possible with ATProto and the Atmosphere.

It's a joke of a term, by and for the true believers.

Adversarial interoperability

You probably haven't heard the term "adversarial interoperability" very often, either. There's an explainer on the EFF site:

This has apparently been rebranded to "competitive compatibility". However, I feel this obfuscates the ways in which our large-tech-platform targets often are our enemies, actually.

Be the adversary you want to see in the world

Let's take these terms and site them in the very specific landscape of ATProto and the Atmosphere. Here are the key points of the program of adversarial internecting:

You want to internect an upstream you don't own: to join it to the Atmosphere using ATProto-native features.

The upstream limits necessary direct access - like DNS, HTTP, or code execution - needed for users to implement their own ATProto integrations.

You want to provide internection-as-a-service to all users of the upstream (not just your own data), and to preserve existing social networks by ensuring that cross-user relationships2 are preserved.

You want upstream users to become Atmosphere netizens, and perhaps even to stop directly posting to the original upstream.

You want to do all this in a way that preserves user consent, but largely ignores the upstream's platform limitations and policies.

In other words, adversarial internecting is not just a one-time export of your own personal data (though that's certainly a good starting point). It's also not transplanting an entire existing site 1:1 onto ATProto. It's intentionally designing a process of ongoing, gradual, and information-preserving transplantation of social networks, from less-open to more-open platforms, with user consent but without asking for upstream permissions. It's about respecting the moral rights of data creators over any platform terms of service.

That is, it is the kind of thing that you'd mainly care about building3 if you are, indeed, a true believer in the Atmosphere.

I'm not really going to talk about your legal rights to take this kind of approach, or what types of trouble you could actually get in. Frankly there are many interesting points, but they are often based on obscure and archaic legislation that isn't fun to discuss with internet strangers.

At a high level, though, I'd like you to consider that legality is more about power and risk tolerance than it is about morality. That's how your upstream corporate platform opponents think about it, and it's one of the reasons they keep winning. There's a lot to learn from what's worked for them and what hasn't!

Maybe that all sounds a bit hostile to you, or like you wouldn't want it happening to your own site. But as a user you might love to have this option! Adversarial internecting enforces the idea of "credible exit" onto platforms that are otherwise hostile to it. Platforms that are not otherwise interoperable or portable, or that could shutter or get sold at any time. Perhaps one that locks access to your data, either behind a paywall or maybe entirely. Maybe even one that you started using before the lock-in issues were apparent.4

In the abstract form, adversarial internecting looks a bit like the strangler vine pattern but applied to entire sites:

Phase 0: Your upstream users get an Atmosphere account at all.

Phase 1: Upstream users' existing data on the upstream5 is migrated to the Atmosphere, once.

Phase 2: Ongoing updates to the upstream are synchronized to users' Atmosphere data.

Phase 3: Updates to users' Atmosphere account data synchronize back to the upstream (bidirectional sync).

Phase 4: Users shift to creating new updates via the Atmosphere first.

Phase 5: Enough user data and friends have migrated that they just terminate your upstream account entirely.

This is very abstract, of course! There are situations where you might skip Phase 1 (not all types of content want backfilling), or Phase 3 (if you want to keep posting on the main site but don't care if it's missing some Atmosphere-first content). Or where you do these phases out of order.

But in general, adversarial internecting is about a progression where you start with your data on a platform with limited export or customization features, work around limitations to gradually become Atmosphere-first as a daily user, and ideally bring all of your friends along for the ride.

Adversarial approaches

There are many ways to adversarially internect a third-party platform (an "upstream") onto the Atmosphere. Before we get into comparing tradeoffs, let's first define some of the more common approaches.

APIs

I won't go into too much detail here, but the ideal case is that an upstream provides either:

A token or API key that users can generate, separate from their account password, that can be provided to other sites/CLIs/extensions to enable read/write functionality.

An "authorize your account with..." in-app flow using OAuth, which is sort of doing the same thing under the hood (but the user never sees the token).

Many sites don't implement either of these. Tokens are often poorly implemented, not available to all users, or hidden behind "Developer Features". Building OAuth apps is complex and subject to upstream approval. And if you use either to process large amounts of data to encourage users to leave the site, you will probably get shut down by the platform.

So that's why we're going to assume you don't have access to do it this way. What other options do you have?

Exporting

Exporting is creating a batch export of account data from an upstream, often in CSV or ZIP-of-CSVs format, and providing it to an Atmosphere app for restructuring and import into your PDS.

A good example of how this looks on ATProto is how sifa.id LinkedIn profile imports work. Here's what my user flow looks like after having already synchronized my LinkedIn once:

A lot of the user experience here is great: it explains how to perform the export and what to expect from it, how your data will be processed, and where it will be visible on the ATProto network (not just on Sifa ID).

Export-based adversarial internection is often the easiest to explain to users, and to implement. In many cases it can be done with no or minimal backend processing. It's also hard to block: your application will never even leave a trail on the upstream, which often has legal requirements (GDPR, CCPA) to maintain these features anyways.

At the same time, it's clearly not going to be pleasant to update exports. Every attempt requires a new login and export, a waiting period, and then manually syncing the data. The exports are also often "schema-free" and poorly structured (like no version info, or missing files if the user "didn't have that content"). This can make it very challenging to write a correct export, and especially to maintain one over time.

Bridging

Bridging involves creating lightweight accounts for users of an upstream, and posting to those accounts on their behalf as they take action on the upstream. Typically, one "end" of the bridge is taken to be the canonical account. This is named in reference to how @ap.brid.gy works.

Bridges are much harder to implement than exports since they require multiple backend components as well as a realtime ordered task queue. This is what the Bridgy Fed architecture looks like:

As seen in the above diagrams, even though it's connecting two decentralized networks together, these architectural needs mean that Bridgy Fed is itself a centralized service. It's not something that a user runs; it's a service that they opt into.

Bridging helps users get started quickly, but there are some real caveats. It's a lot harder to support bridging into existing accounts, especially arbitrary accounts on arbitrary services. That means taking on the complexity of delegating authentication, or having complex workflows to let a user "take over" an account you made on their behalf: it's only even recently become possible to migrate Bridgy-created accounts into "real" accounts. And if your bridge service isn't the only writer to the bridged account, that may introduce significant additional data complications.

Bidirectional bridges compound data format mismatch issues by needing a bidirectional mapping. This can lead to "lowest common denominator" approaches across social networks, where only the features that work "reasonably well" on both get implemented. You can see some of those limitations by reading the Bridgy Fed documentation:

As a point of clarification, what makes bridging "adversarial" as a general approach is that you can implement it without platform permission. This is very different from obtaining user consent. Not respecting this distinction caused a deeply negative reception to Bridgy Fed's original opt- out approach.

That said, I do not want anyone leaving with the impression that this makes Bridgy Fed a bad actor! In fact, they've spent a ton of time subsequently building consent and moderation features in at various levels. You can read about this in their documentation.

Sidechanneling

Even many platforms too locked down for user APIs offer less-restricted data feeds: notification emails, events synchronized into calendar invites, and/or RSS feeds. Due to the limitations of the consuming platforms, it's often hard to restrict consumers of these syndicated artifacts or to change the data model without legacy clients breaking. These feeds also often have simple authentication, such as a "magic URL" that embeds an auth token.

All of these properties are highly beneficial for adversarial internection!

For example, "onboarding" might involve telling users to turn on email notifications in the upstream, and then adding a prewritten forward rule in Gmail to a user-specific inbox maintained by the Atmosphere app. The upstream won't even know the user is making use of this feature since it will just show up as standard Gmail traffic.

Scrapers

One of the oldest approaches to adversarially consuming content is to simply scrape it from the upstream directly! This is how most modern LLMs were trained, of course; but even pre-LLM, many apps did this to other apps in order to populate their initial datasets.

Traditional scraping looks like watching a set of URLs (like user profiles), parsing their contents locally, and periodically looking for changes. Sometimes you'll need to hit multiple endpoints in a sequence (like the user profile to see that new content exists, and then the page with the new content to get its full metadata).

Implementing scraping at scale has gotten significantly more challenging as more platforms have become aware of the value of their data and the potential cost of automated actors. That means thinking about CAPTCHAs, CDN bot detection, IP banlists, and more.

However, there's another compelling option in this category: local scraping on a user-by-user basis.

I use @monarchmoney.bsky.social to manage my expenses; it sees purchases from "Amazon" come through via an API connection to my credit card. But my credit card just gets a vendor and a dollar amount, and has no idea what I bought on Amazon! Those orders could be from any number of stores or for any type of product, but default to being categorized as "Shopping".

This is a hard problem for a startup like Monarch, because Amazon doesn't ship a consumer purchases API and they have no leverage to push for one.

Their solution?

They wrote a browser extension that can see purchases and retrieve product names (locally), then run them through a categorization backend (in the cloud). Monarch's servers never directly talk to Amazon's, but as long as I stay logged into both in my browser, I'll get much better categorization: each order split per-item into "Groceries" or "Clothing" or "Home Improvement".

The closest Atmospheric equivalent is likely how Sky Follower Bridge works. In that case it's an explicit synchronization action rather than implicit in the background, making it closer to the Exporting flow. But ultimately they're both local scrapers implemented as browser extensions, making it harder for the upstream to do anything about it.

I think it's worth noting that while wide adoption of AI has made server-side scraping at scale a lot harder; it's gotten a lot easier to actually write and maintain scraping scripts, and especially ones that work locally. Claude is simply quite good at building a CLI or browser extension that works end-to-end from relatively few examples.

Proving

Before we talk about proving, we need a little context.

When you want to link Atmosphere apps to non-Atmosphere data, the most "Atmospheric" way of doing this is a handshake. First, you point a DNS record (or HTTP .well-known) to your ATProto PDS. Ideally this is also bidirectional where you then write a record in your PDS that points back to the DNS record. You're likely familiar with this flow if you have ever set a custom Bluesky username.

A similar flow can be used at the HTML level with rel= links for standard.site document ownership 6. This verifies that the blog is owned by a PDS, instead of the PDS just publishing records for someone else's blog.

But what if you don't control DNS or HTTP on the other end? Or even HTML? This is common in "closed social" sites like Facebook or GitHub, where you have some measure of public customization but typically cannot embed full HTML.

This is where proving enters the picture. It's akin to a special case of scraping: add a link somewhere in your public profile pointing back to your Bluesky account (or a cryptographic record based on it) to establish a linked identity that crosses platform boundaries.

There haven't been many "proof-based" applications on ATproto, but Keybase made very good use of it (before their inevitable decay from crypto adoption):

Proving deserves special mention in the adversarial internecting case. Many of the discussed techniques, like scraping, could work on any account... but often, you don't want them to. It's a major violation of user consent to implicitly create user profiles (that's why Meta did it!), and it's even worse if you allow random users to scrape others' profiles without proof of ownership.7

This means that proving ownership can be a good first step before accepting any migrations. It can be thought of as an alternative to the bridging model: instead of determining ownership by creating the account, you determine ownership by who controls both accounts at the same time.

The recap

Each of these techniques comes with different tradeoffs. Let's look at some common ones:

User Effort: How hard is it for a user to set this up?

Maintainer Effort: How hard is it for the service maintainer to set this up and afford to keep it running?

Data Latency: How fast are updates propagated?

Data Fidelity: How close is the on-ATProto data to the original data?

Bidirectionality: How close does this get us to a bidirectional update?

Composability: How easy is this approach to compose with other approaches to get a full picture?

Service Risk: Is the service providing the data transfer likely to be reliable (apart from upstream issues)?

Upstream Risk: How easy is it for the upstream to block access through this route?

We can see that there simply isn't an option that's green across the board. In fact, there isn't much green in the first place! This is not an approach for the faint of heart.

Other problems

Of course, one blog post can't cover every kind of risk or caveat. Other considerations include whether and how to do backfills, whether your architecture misses posts if the service or upstream are temporarily down, and avoiding getting your users' accounts banned at the upstream (which often prevents any of these approaches from working, even data exports).

But here are a few problems I wanted to call out in particular:

What does friendship really mean?

There are all kinds of "friend" and "follower" implementations across social media, and they do not necessarily map cleanly to the ATProto follower relationship: a reviewer I follow on Letterboxd isn't necessarily my "friend" on ATProto. (Maybe they're better imported as a List.)

In the long term, it's maybe even unclear whether you want "all" types of friends and content within a single ATProto DID. Very few people actually want "the everything app".

Order matters

The previously-mentioned Sky Follower Bridge was one of many attempts to implement "exit from X". It focuses on followers and uses a hybrid approach that includes direct username matching, fuzzy matches, and direct Bluesky links on your profile.

This worked really well... and if you used it early, it missed most of your followers, because they hadn't migrated to Bluesky yet.

Most platform migrations will follow a similar pattern to X, where users move one-by-one. If you implement Exporting or Scraping (Local): how does your first user get notified when friends 2, 3, ...n sign up for the site? Introducing this as an ongoing capability is significantly more effort than the flawed one-time case.

Object references are painful to migrate into decentralized systems

Similar situations occur if a user's content has references to other users that haven't migrated yet. For example, posts with a @-reference to a user typically get dumped into plaintext. On a reference-heavy site like Letterboxd, missing any referenced data could seriously degrade the user experience.

If you take a Letterboxd export, this is what shows up:

If you've been a DBA before, you might immediately notice that Letterboxd actually runs on a composite primary key: movie title, and year of release.

It's worth noting that a movie can be released with multiple titles, or even multiple English titles8. And there are many ways to calculate release year - in fact, Letterboxd's definition means the release year could change if new historical information becomes available.

This makes this key choice problematic for a decentralized system. If different people can make different reasonable choices - then they will. How do you ensure that the distributed data remains standardized enough that multiple people can build on it as an API?

Let's continue to pick on Letterboxd as an example.

In an adversarial internection scenario, the long term goal would be for "Movie" records to be an ATProto record. Your Letterboxd clone could use those when writing Reviews records... but someone else could make Atmosphere apps for "movies your followers like" recommendations, movie trivia, documenting your physical media collection, etc.

But there are many challenges that would have to be ironed out:

What if a rightsholder and a director both create "Movie" records?

Deduplication can help, but which one is the canonical one that user PDS "Review" records point to?

What if the user holding the canonical Movie record deletes their account? Review records will point to something non-existing.

How do you deal with a huge collection of records owned by someone else, and directly referenced by other users, if you need to extend the Lexicon type in ways the owners don't agree with?

Should references to "people" be their ATProto profile (if it exists), a stable identifier, a string of their name at the time?

What if information is partially public domain, and partially copyrighted (like movie posters)? Does this expose the hosting PDS to any liability?

My own suspicion is that getting large, complex, and referenceable datasets "on-proto" will be solved by stable consortiums. This is roughly what Standard Site is doing: design a "blog" data structure and they collectively implement and maintain it.

But it's harder when we're moving beyond ontology ("how is a 'Movie' record structured") and into a canonical dataset ("what 'Movies' exist in the world and who is in them"). That will require exponentially more judgment and maintenance, and some hard calls that make people unhappy.

The missing middle

This is a non-technical problem, but a very interesting one.

Your first users will be power users, and will show up even if nobody else is on the app yet. That's what it was like in the first months of Bluesky, when your feed might have no activity at certain times of the day.

Your last users will be people who hear about "the new Letterboxd", see all their friends already on it, and migrate without really thinking about it hard.

But what about the users in the middle? This is where adversarial internecting gets hard:

Since you respect your users, you don't want to move anyone's data to the Atmosphere until they provide consent. But if they do initiate it, you want to have enough populated data that users have a good experience and don't immediately churn. And it's hard to hit that critical mass!

I think the answer here is good old fashioned growth hacking:

Get heavy users of both Atmosphere and Letterboxd to join your site before most users, in order to seed the site with popular and populated accounts. Ideally, get commitments from a couple famous users to reskeet you on launch day.

Batch users into "cohorts" based on metrics that you think make them compatible. This could include: simclusters, geographical region, amount of existing upstream content. Then launch cohorts as waves, so that new users immediately see other new content that keeps them present.

Pull in aggregate or deidentified "filler" data from non-Atmosphere sources so that users have a better experience as early interactors with content. Gradually migrate data to be Atmosphere-sourced as enough users contribute records.

I think it would be a better world if you didn't have to think about app launches in such an instrumental way, but unfortunately you do. Bluesky did this with invite codes and starter packs; Threads did this by bootstrapping with Instagram followers. Atmospheric apps are still subject to competition for attention and user experience, so keep your principles in mind, but don't be afraid to get your hands a bit dirty.

So you want to heist an app

Given all of the above, what kind of approach would I take to move every user on Letterboxd to the Atmosphere?

Start by designing the data model. I would personally build this as a hybrid between user PDS (Reviews), and references to records in a central PDS (Movies, Actors). The new "permissioned spaces" data model is likely a good fit for this.

Populate enough data about existing movies as ATProto records so that you can test reference functionality (like a Review referencing a Movie which references an Actor).

Use Claude to prototype a cloned version of the app, against the new data records. The initial goal is to have all data stored in the PDS with no app backend (at this stage). Use this to test viewing and writing new records via OAuth flows.

Implement an app backend for indexing and efficiently searching on-proto data records. Then integrate that into the "Add a review" feature so that users can quickly populate metadata referring to specific movies.

Implement an in-browser Letterboxd export, which uses the backend to try to map Letterboxd data to the equivalent Movie records in the dataset (and their canonical ATProto record information).

At this point, you actually have a "working app" - and with Claude you could probably have it working in a day if you're sufficiently motivated. But while this is something you could hand to an existing Letterboxd user, the experience will be a bit rough - so it's best to limit it to friendly test users.

What's after that?

Implement Proving so that users have to show Letterboxd account ownership before the system accepts any exports or scrapes.

Extend the existing Exporting mechanism with a new Sidechanneling (RSS feed) or Scraping feature to pull in new reviews. This lets you combine historical backfill that's crucial for existing Letterboxd users, with additional data arriving as it's written.

At this point, you've got an ATProto app that basically works like Letterboxd; and if you keep posting updates to Letterboxd they'll all show up on the Atmosphere too. The main downside is that your Letterboxd followers won't see the Atmosphere half of your updates.

So, how do you get to mass adoption? This is always the hardest part of building social apps, because it's the least technical. But I do have some less-technical thoughts:

Build something new that you need to be on the Atmosphere to access. A game that uses the data, recommending who to follow, finding your actors' social media feeds - something that encourages people to want to move.

Consider Bridging via a new PDS server geared towards getting upstream users quickly onboarded to the Atmosphere (like for playing an on-proto game). But you don't want to be in the business of running a PDS, so provide a migration pathway to get them on a "full" Atmosphere account ASAP.

Make your on-proto data available to other people to build new features that use your users' data. This means: document everything, announce upcoming changes early and clearly, make sure it's as open as feasible. Even if it cannibalizes some of your future roadmap.

Investigate bidirectional sync. This is often going to be the hardest part, but it's worth checking out at least for the power users. Maybe a local browser extension that POSTs to Letterboxd as new PDS records are written?

Get yourself increased visibility via platforms like the ATProtocol App Directory or at.fund

Work with other ATProto builders to come up with cross-promotions and integrations that make sense. Off the top of my head: Leaflet embeds for Reviews? Roles in movies on Sifa ID? Auto-generation of Bluesky Lists or Starter Packs from lists of favorite actors?

To 'nect or not to 'nect

Now, am I saying that someone should do all of this for Letterboxd? No, not necessarily9: it's a pretty open platform and any acquisition isn't likely to change that immediately.

But there is another benefit to thinking through the how and why of this approach, even if you don't use it on today's VC-backed startups or big tech walled gardens.

The Atmosphere is a remarkably open place right now, but there are all kinds of "short-term" sensible approaches that could reduce "long-term" openness:

implementing Atmosphere features to drive traffic to a site, but no features on the site that bring in Atmospheric content and interaction

rewriting common Lexicon user records, into site-specific Lexicons to provide "extended data"

moving records into app-restricted private spaces when they could have been open

moving key data off-proto entirely

There isn't much "antisocial behavior" among Atmospheric developers yet, but that's often true of early adopters who arrive before something is profitable. If the Atmosphere keeps growing, we will see an influx of builders who don't share the same values as early adopters. The practice of adversarial internecting has a role to play in keeping Atmosphere participants honest:

If you won't do your part to keep data open, we'll do it for you!

I'm pretty invested in this, as I've been using it to log my goal of watching a new-to-me movie every week over the last few years!

Think @-tagging users, groups, or friends lists.

Like how my Steam account is permanently locked to my 20+ year old childhood email address that I don't even own anymore.

There is some ongoing discussion about whether this should actually be a per-article head link vs. some other HTML, since this is hard to implement per-page in many templating systems. Sorry, can't find the reference right now!

For example, this is frequently used to quickly populate "realistic-looking" fraud accounts.

The film "Edge of Tomorrow" originally entered production as "All You Need Is Kill" - and would've been listed as that on Letterboxd at the time. It was released by some marketers as "Live Die Repeat: Edge of Tomorrow". Confusingly, the original title in English was the Japanese title of the light novel it's based on... which now has a recent film adaptation with the same name.

See how this kind of thing can be a problem?

But if someone did, I would give it a try!