In Part 1, we explained what CIDs are: self-describing, content-addressed identifiers. We discussed their origins in Git and BitTorrent, their development through IPFS, Multiformats, and IPLD, how they are encoded using a multicodec version and codec prefix over a multihash, and the constraints ATProtocol sets: only CIDv1, only SHA-256, two codecs, and a fixed size of 36 bytes.
We also introduced a key concept in ATProtocol’s data model: AT-URIs are location-addressed, meaning they name a mutable slot in a repository, while CIDs are content-addressed and name an immutable piece of content. An AT-URI serves as a label that can change location, whereas a CID is a fingerprint that remains the same.
In this part, we continue exploring the AT-URI and CID duality. This forms the basis of a versioning system, with the repository’s logical clock providing the timeline. We will explain this model, show how CIDs are created in practice, and follow their movement through repositories, the Merkle Search Tree, inter-record links, and the sync protocol.
Naming, Versioning, and Time
AT-URIs as mutable labels on immutable content
At any time, an AT-URI points to a CID. When you use com.atproto.repo.getRecord, the PDS checks the current Merkle Search Tree and returns the record data with its CID. The CID serves as the actual record, because it is computed directly from the record’s DAG-CBOR serialized bytes rather than being stored as separate metadata. The MST leaf entry for that record’s path stores the CID as its value pointer.
This means the AT-URI to CID mapping changes over time. For example, at://did:plc:abc/app.bsky.feed.post/3k2la7fx2jc22 might currently resolve to bafyreig5.... If the record is updated, such as editing the post text, the same AT-URI now points to bafyreih7.... The old CID is still valid and identifies the old content, but the AT-URI no longer refers to it.
This is similar to how a variable name relates to a value in a program. For example, the name x might hold 42 at one time and 99 later. The name can change its value, but the values themselves do not change. You can ask, “what does x hold right now?” or “is this value 42?”, but these are different questions with their own stability guarantees.
AT-URIs with handles are doubly mutable because the handle can change if the user moves to a new domain, and the content can change if the record is edited. AT-URIs with DIDs are singly mutable: the identity stays the same, but the content can still change. Only a CID is truly immutable, as it is permanently tied to its content by the SHA-256 algorithm.
AT-URI scheme specification: https://atproto.com/specs/at-uri-scheme
ATProtocol repository spec (record retrieval, MST structure): https://atproto.com/specs/repository
ATProtocol data model (CID links, record structure): https://atproto.com/specs/data-model
StrongRef: pinning the binding
ATProtocol recognized that many situations need to freeze the AT-URI to CID binding at a specific moment. This lets you say not just “I liked this post,” but “I liked this specific version of this post.” The com.atproto.repo.strongRef type is designed for this purpose:
{
"uri": "at://did:plc:abc/app.bsky.feed.post/3k2la7fx2jc22",
"cid": "bafyreig5..."
}A StrongRef saves both the name and the fingerprint. If the referenced post is edited later, the StrongRef still points to the original version. The CID acts as an integrity check: “the content at this AT-URI, at the time I referenced it, had this exact CID.”
Devin Ivy of the Bluesky team described this pattern in a GitHub discussion: a CID in a like record serves as “a qualifier to support an integrity check or strong reference… so that if the record is modified, the liker has documented the specific record version they liked — useful if the record has changed in some considerable way.”
StrongRefs are used throughout the Bluesky application lexicons. For example, app.bsky.feed.like stores both the liked post’s URI and CID, locking in the exact version that was liked. app.bsky.feed.repost does the same for reposted content. Reply references in app.bsky.feed.post include both a parent and root StrongRef. The pattern is clear: any cross-repo reference that needs version pinning uses a StrongRef.
The Bluesky team has also considered a “strong reference URI,” which would be a single AT-URI string that includes the CID directly, such as at://did:plc:abc/app.bsky.feed.post/3k2la7fx2jc22#bafyrei.... This would combine the two fields into one string while keeping the version-pinning feature.
com.atproto.repo.strongReflexicon definition: https://lexicon.garden/nsid/com.atproto.repo.defsapp.bsky.feed.likelexicon (StrongRef in practice): https://lexicon.garden/nsid/app.bsky.feed.likeapp.bsky.feed.postlexicon (reply StrongRefs): https://lexicon.garden/nsid/app.bsky.feed.postDevin Ivy's discussion on CIDs in like records: https://github.com/bluesky-social/atproto/discussions/1127
The triple: (AT-URI, CID, rev)
In addition to the AT-URI and CID, time is an important third element. ATProtocol highlights the importance of this time aspect by associating each AT-URI and CID link with the repository’s logical clock.
Every commit to a repository carries a rev, a TID (Timestamp Identifier) that the specification explicitly describes as a “logical clock.” TIDs encode microseconds since the UNIX epoch in a base32-sortable, 13-character string. They must be monotonically increasing within a repository: each commit’s rev must be greater than the previous commit’s rev.
Each rev corresponds to an atomic snapshot of the entire repository. The signed commit at that revision contains a data CID pointing to the MST root, and the MST root determines every AT-URI + CID binding in the repo at that point in time. A single rev` implies a complete, consistent mapping:
rev → commit CID → MST root CID → { AT-URI₁ → CID₁, AT-URI₂ → CID₂, ... }The triple (AT-URI, CID, rev) is a versioning primitive with three specific guarantees.
Ordering: The rev provides a complete order of all states in a single repository. If you observe the same AT-URI at two points, (CID₁, rev₁) and (CID₂, rev₂), and rev₂ is greater than rev₁, then CID₂ is the newer version. If CID₁ equals CID₂, the record did not change between those revisions; only other records in the repository changed.
Consistency: Since each rev matches a signed commit, all AT-URI to CID bindings at that rev are internally consistent. There are no “torn reads”; you cannot see some records from one version and others from a different version. The MST root CID at a given rev determines the entire state.
Verification: With a rev and its signed commit, you can check every AT-URI to CID binding by walking the MST. The commit signature covers the root CID, and the CID values flow through the tree to every leaf. You do not need to trust the PDS. Anyone with the commit and tree data can verify the entire snapshot independently.
ATProtocol’s rev is a Lamport timestamp, which is a single counter that only increases and is managed by one process. Here, that process is the repository’s main PDS. The rev gives a total order of all commits within a single repository. However, it does not order commits across different repositories, since there is no causal link between rev values from different DIDs.
The sync protocol uses rev to keep clocks in sync. The since field in com.atproto.sync.subscribeRepos commit events shows the observer’s clock position: “this diff is relative to rev N.” When a relay asks for a repo diff, it is requesting “give me everything since rev X,” which updates its local view of the repo’s timeline.
If the since value does not match the observer’s last known rev for a DID, the repo is marked as out-of-sync, similar to finding a gap in a Lamport clock sequence. Because rev values use the TID format (which is timestamp-based), any revs that seem to be in the future, beyond a small allowed clock drift, are rejected. The logical clock is loosely tied to real-world time.
The Bluesky team’s 2023 repo sync update described this directly: “If a consumer encounters the same repo from two different sources, each with a valid signature and structure, the revision gives a simple mechanism to determine which is the most recent repository.”
Lamport timestamps (Wikipedia): https://en.wikipedia.org/wiki/Lamport_timestamp
ATProtocol sync spec (
rev,since, subscribeRepos): https://atproto.com/specs/synccom.atproto.sync.subscribeReposlexicon: https://lexicon.garden/nsid/com.atproto.sync.subscribeReposBluesky repo sync update (2023,
revas ordering mechanism): https://docs.bsky.app/blog/repo-sync-updateTID format specification: https://atproto.com/specs/record-key#record-key-type-tid
The MVCC mental model
If you have a background in databases, you might notice this pattern is Multi-Version Concurrency Control (MVCC). In this model, the AT-URI is like a row key, the CID is the cell value, and the rev is the version timestamp.
┌───────────────────────────────┬──────────────┬───────────────┐
│ AT-URI │ CID │ rev │
├───────────────────────────────┼──────────────┼───────────────┤
│ .../app.bsky.feed.post/abc123 │ bafyreig5... │ 3k2la7ax1zz99 │ created
│ .../app.bsky.feed.post/abc123 │ bafyreih7... │ 3k2la7fx2jc22 │ edited
│ .../app.bsky.feed.post/abc123 │ bafyreih7... │ 3k2la7gx2ab34 │ unchanged
│ .../app.bsky.feed.like/def456 │ bafyreij3... │ 3k2la7fx2jc22 │ created
│ .../app.bsky.feed.like/def456 │ (deleted) │ 3k2la7hx3cd56 │ unliked
└───────────────────────────────┴──────────────┴───────────────┘Each rev acts as a transaction ID. When you “read at rev R,” you get the latest CID for each AT-URI where rev is less than or equal to R. This is exactly what repo sync does: it asks for the state at or since a specific rev.
The MST acts as the index that makes this process efficient. Instead of scanning every record to find changes, the MST recalculates CIDs from the point of change upward. When a record is modified, its CID changes, which then changes its parent MST node’s CID, and this continues up to the root. The signed commit at each rev serves as the “transaction commit record” that makes the snapshot both durable and verifiable.
ATProtocol’s version of MVCC is different from what you find in traditional databases. In PostgreSQL or MySQL, MVCC depends on trusting the database engine to manage version history correctly. In ATProtocol, MVCC is externally verifiable. Anyone with the signed commit can independently rebuild and check the entire snapshot. The CID makes this possible, turning “trust the server’s version history” into “verify the math yourself.”
This is what makes account portability possible. You can export a snapshot as a CAR file, move to a new PDS, and anyone can check that the new host has the same data by verifying CIDs from the signed commit down. The version history is not kept in a proprietary database engine; instead, it is stored in a chain of content-addressed, cryptographically signed commits that anyone can verify.
Multi-Version Concurrency Control (Wikipedia): https://en.wikipedia.org/wiki/Multiversion_concurrency_control
ATProtocol account portability overview: https://atproto.com/guides/account-migration
Creating CIDs: Step by Step
Now that we have covered the theory, let’s look at the practical steps. This section explains how to create a CID for the two types of content ATProtocol uses: structured records (dag-cbor) and binary blobs (raw).
Record CID creation (dag-cbor, 0x71)
We will walk through the full process using a simple app.bsky.feed.post: a "Hello, world!" post with the three fields that every post includes:
{
"text": "Hello, world!",
"$type": "app.bsky.feed.post",
"createdAt": "2025-02-20T12:00:00.000Z"
}Step 1: Serialize as DAG-CBOR. Take the record data and serialize it using deterministic CBOR rules. This is where most of the complexity lives.
The first rule is key ordering. Map keys must be sorted by byte length first, then lexicographically within the same length (RFC 7049 §3.9 canonical CBOR ordering). For our three keys, that means:
"text" 4 bytes
"$type" 5 bytes
"createdAt" 9 bytesThis creates a key order that may seem unusual if you are used to alphabetical JSON, since text comes before $type, but it is correct under DAG-CBOR rules. Shorter keys always come first. If keys have the same length, they are sorted lexicographically (for example, $type at 5 bytes would come before langs at 5 bytes, because $ (0x24) comes before l (0x6C)).
The second rule is to use minimum-width encoding. Integer and length values must use as few bytes as possible. For example, a string with 13 characters uses a 1-byte length prefix (0x6D), not a 2-byte or 4-byte encoding.
There are more constraints: no indefinite-length items, no duplicate map keys, no floats (ATProtocol does not allow them), no NaN, no Infinity, and no undefined values. When present, CID links are encoded as CBOR tag 42 (0xD82A), which wraps a byte string starting with 0x00 (the multibase identity prefix) followed by the 36-byte binary CID. Our simple post does not have links, but a reply or quote post would.
Serializing our "Hello, world!" post results in exactly 81 bytes. Here is the complete breakdown, byte by byte:
a3 map(3)
64 74657874 "text" (4 bytes)
6d 48656c6c6f2c20776f726c6421 "Hello, world!" (13 bytes)
65 2474797065 "$type" (5 bytes)
72 6170702e62736b792e666565642e706f7374 "app.bsky.feed.post" (18 bytes)
69 637265617465644174 "createdAt" (9 bytes)
7818 323032352d30322d32305431323a "2025-02-20T12:00:00.000Z" (24 bytes)
30303a30302e3030305aThe a3 byte is a CBOR map header: major type 5 (map) with additional info 3 (three entries). Each key-value pair follows as a text string header (major type 3 + length) and then the UTF-8 bytes. The createdAt value at 24 bytes is long enough to need a 2-byte length prefix (78 18 major type 3 with additional info 24, meaning "1-byte length follows," then 0x18 = 24).
The output is a deterministic byte array. Determinism is essential: the same record data must always serialize to the exact same bytes, because any difference would create a different CID. You can check this yourself and any conforming DAG-CBOR encoder given this record will produce the hex string a364746578746d48656c6c6f2c20776f726c6421652474797065726170702e62736b792e666565642e706f7374696372656174656441747818323032352d30322d32305431323a30303a30302e3030305a.
Step 2: Hash with SHA-256. Compute the SHA-256 hash of those 81 bytes. For our post:
b38bc4817b97820bcb7cf1025d35673dc8d8545759b34b067fe44da9a5909b71That's our 32-byte digest.
Step 3: Assemble the 36-byte binary CID. Prepend the four single-byte prefixes:
[0x01] [0x71] [0x12] [0x20] [32-byte SHA-256 digest]
│ │ │ │ └── b38bc481...a5909b71
│ │ │ └── digest length = 32
│ │ └── SHA-256 hash function
│ └── dag-cbor content codec
└── CIDv1 versionThe complete 36-byte binary CID in hex:
01711220b38bc4817b97820bcb7cf1025d35673dc8d8545759b34b067fe44da9a5909b71Step 4: Encode as a string. Base32-encode the 36 binary bytes using the lowercase RFC 4648 §6 alphabet, then prepend the b multibase prefix:
bafyreiftrpcic64xqif4w7hrajotkzz5zdmfiv2zwnfqm77ejwu2lee3oeThat is the CID of our "Hello, world!" post. It starts with “bafyrei”, which is the prefix for every dag-cbor and SHA-256 CID. If you change even one character in the post text, the createdAt timestamp, or the $type string, you will get a completely different CID.
In pseudocode:
function createRecordCID(record):
// generate deterministic CBOR
cbor_bytes = dag_cbor_serialize(record)
// generate 32-byte hash
digest = sha256(cbor_bytes)
// 36 bytes binary CID
binary_cid = [0x01, 0x71, 0x12, 0x20] ++ digest
// string serialize binary CID
return "b" ++ base32_lower(binary_cid) Blob CID creation (raw, 0x55)
Step 1: Read the raw bytes. There is no serialization step. The blob, such as an image, video, or audio file, is hashed exactly as it is, in full. There is no chunking, DAG construction, or CBOR encoding.
Step 2: Hash with SHA-256. Compute the SHA-256 hash of the complete file bytes. Same algorithm, same output: a 32-byte digest.
Step 3: Assemble the 36-byte binary CID. The only difference from a record CID is byte 2 — the codec:
[0x01] [0x55] [0x12] [0x20] [32-byte SHA-256 digest]
│ │ │ │ └── the hash output
│ │ │ └── digest length = 32
│ │ └── SHA-256 hash function
│ └── raw content codec
└── CIDv1 versionStep 4: Encode as a string. Same base32 process. The result starts with “bafkrei”.
The two processes are the same except for serialization (DAG-CBOR for records, none for blobs) and the codec byte (0x71 for records, 0x55 for blobs). Here they are side by side:
function createBlobCID(file_bytes):
// generate 32-byte hash
digest = sha256(file_bytes)
// 36 bytes binary CID
binary_cid = [0x01, 0x55, 0x12, 0x20] ++ digest
// string serialize binary CID
return "b" ++ base32_lower(binary_cid)Parsing a CID string back to bytes
Parsing works in reverse: remove the first character (which must be b), base32-decode the rest, and then check each field. Byte 0 must be 0x01 (CIDv1). Byte 1 must be 0x55 or 0x71. Byte 2 must be 0x12 (SHA-256). Byte 3 must be 0x20 (32). There must be exactly 32 bytes of digest, making a total of 36 bytes. Reject anything that does not match, such as CIDv0, the wrong hash, the wrong codec, or the wrong length.
RFC 7049 §3.9 (canonical CBOR map key ordering): https://datatracker.ietf.org/doc/html/rfc7049#section-3.9
RFC 8949 (CBOR, successor to 7049): https://datatracker.ietf.org/doc/html/rfc8949
DAG-CBOR codec spec (determinism rules, tag 42): https://ipld.io/specs/codecs/dag-cbor/spec/
CBOR tag 42 specification (CID links): https://github.com/ipld/cid-cbor/
RFC 4648 §6 (base32 lowercase alphabet): https://datatracker.ietf.org/doc/html/rfc4648#section-6
Multibase spec (the
bprefix): https://github.com/multiformats/multibaseDASL CID spec (ATProtocol CID constraints): https://dasl.ing/cid.html
Implementation references
Several libraries provide ATProtocol-specific CID implementations:
The dasl Rust crate (the n0-computer team) defines Multihash::Sha2256 and Multihash::Blake3 variants alongside Multicodec::DagCbor and Multicodec::Raw. It uses sha2 for SHA-256, blake3 for BLAKE3, data-encoding for base32, and cbor4ii for CBOR parsing.
The @atcute/cid TypeScript package (successor to @mary/atproto-cid) provides a minimal implementation where create(0x71, buffer) hashes a buffer and returns a CID object with version: 1, code: 113, and bytes: Uint8Array(36).
The cid Elixir hex package provides CID.cid!(data, "dag-cbor", "sha2-256") to create CIDs from serialized data.
The atproto-dasl crate from the atproto-identity-rs repository provides another Rust reference implementation focused on ATProtocol’s specific constraints.
I highly suggest the actively maintained https://sdk.blue for a more complete list of projects and SDKs.
ATProtocol Real-World Usage
Repository structure and the Merkle Search Tree
Every ATProtocol user’s data lives in a repository: a signed, content-addressed data structure. At the top is a commit object (version 3):
{
"did": "did:plc:abc123",
"version": 3,
// CID for the MST root
"data": { "$link": "bafyrei..." },
// Lamport clock
"rev": "3k2la7fx2jc22",
"prev": null,
// cryptographic signature
"sig": "<bytes>"
}The data CID points to the root node of the Merkle Search Tree. The rev is the Lamport clock value for this commit, serving as the time marker that gives a total order to all repository states and allows the sync protocol to keep clocks in sync. The sig covers the DAG-CBOR serialization of the entire commit object. To verify, you check the signature and then verify every CID down through the tree.
The MST maps record paths (like app.bsky.feed.post/3k2la7fx2jc22) to record CIDs. Each MST node is a DAG-CBOR object using compact single-character field names: l for the left subtree CID, and e for an entries array. Each entry contains p (the number of prefix bytes shared with the previous key), k (the remaining key suffix), v (the CID of the record data), and t (a right subtree CID, or null).
Tree depth for a given key is found by hashing the key with SHA-256, counting the leading binary zeros in the hash output, and dividing by 2 (rounding down). This results in a fanout of about 4 per level. The same SHA-256 that secures CIDs also shapes the tree.
Each MST node has its own CID. When a record changes, its CID changes, which then changes the parent MST node’s serialization and its CID, and this continues up to the root. This index structure makes the MVCC model efficient. Instead of scanning every record to build a diff, you only need to follow the path of changed CIDs from root to leaves.
The verification chain works in the opposite direction. You start from the signed commit, move to the MST root CID, and then walk through the tree. At each step, you can independently calculate the CID of the data you received and compare it to the claimed CID. If they all match, the entire repository—every record and every tree node—is authenticated by the single signature at the top. This is how account portability works: export the commit and all blocks, and any server can verify the repo on its own. Each rev snapshot can be checked independently, since the signature at the top covers every AT-URI to CID binding in the repo.
DASL DRISL spec (deterministic serialization constraints): https://dasl.ing/drisl.html
DAG-CBOR serialization
The determinism requirements for DAG-CBOR are essential. If the same data could serialize to different byte sequences, it would create different CIDs, and the entire Merkle tree would not work. If two implementations serialize the same record differently, they would compute different CIDs and disagree about the repository’s state.
The rules are strict. Map keys are sorted by byte length first, then by lexicographical order within the same length. Integer and length encodings must use the minimum number of bytes. There are no indefinite-length containers or duplicate map keys. ATProtocol also does not allow floating-point numbers (the data model uses only integers and strings), undefined values, NaN, or Infinity.
CID links in DAG-CBOR use CBOR tag 42 (0xD82A), which wraps a byte string that starts with 0x00 (the multibase identity prefix) and continues with the 36-byte binary CID. The full CBOR encoding of a single link is 41 bytes: 2 bytes for the tag, 2 bytes for the byte string header (length 37), 1 byte for the identity prefix, and 36 bytes for the CID. Tag 42 is the only CBOR tag used by ATProtocol.
Links between records
CIDs appear in records through two distinct mechanisms, and the choice between them is intentional.
Binary cid-link (CBOR tag 42) is used for links within a repository. MST nodes linking to other MST nodes, MST leaves linking to records, blob references — these all use the binary link format. In JSON API responses, they render as {"$link": "bafyrei..."}. IPLD tools follow these links during traversal. A blob reference in a post’s embed looks like:
{
"$type": "blob",
"ref": { "$link": "bafkreibjfgx2gprinfvicegelk5ko..." },
"mimeType": "image/jpeg",
"size": 482349
}String-format CIDs in StrongRefs are used for cross-repo references where version pinning matters. A plain string CID field sits alongside an AT-URI in the com.atproto.repo.strongRef structure. The CID pins the exact version of the referenced record and is this is the “freezing the AT-URI + CID binding” pattern in practice.
Why use two mechanisms? String CIDs do not cause IPLD tools to follow cross-repo references. If every like record’s reference to a liked post was a binary CBOR link, an IPLD walker would try to fetch every post you have ever liked from other people’s repositories, which would not work well at network scale. String CIDs are hidden from IPLD traversal; they still provide version-pinning information, but they do not create links in the content-addressed graph.
The design is straightforward: links within a repository are binary (so they can be traversed and verified as part of the repo DAG), while cross-repo references are strings (not traversable, but still pinning a specific version). A repository is a self-contained, verifiable unit. To verify across repositories, you need to fetch the other repo. In a StrongRef, the CID turns a mutable AT-URI into an immutable reference point, which is the simplest form of versioning.
Links between records
IPLD data model spec (link traversal): https://ipld.io/specs/data-model/
ATProtocol blob spec (blob references): https://atproto.com/specs/blob
CAR files: packaging and transport
CAR (Content Addressable aRchive) is the format used to package and move content-addressed data. ATProtocol uses CARv1, which has a simple structure.
The file starts with a header: a varint-encoded length prefix, followed by a DAG-CBOR object containing version: 1 and a roots array of one or more CIDs. The roots array tells the reader where to start traversing the data.
The rest of the file is a series of blocks. Each block has a varint-encoded length prefix, the block’s CID in binary, and the block’s data bytes. That’s all—a flat stream of CID-tagged blocks.
For a full repository export, the first root is the CID of the latest commit. The CAR file must include the commit, all MST nodes, and all records. Blobs are not included; they are stored and fetched separately. Anyone receiving the file can verify each block by computing its CID from the block data and comparing it to the claimed CID. This is the MVCC snapshot saved to disk: a complete, self-verifying, point-in-time image of every AT-URI to CID binding in the repo.
For repository diffs, com.atproto.sync.subscribeRepos sends diffs as CAR slices. These slices include only the blocks that changed: new or updated records, new MST nodes, and the new commit. Deleted records are shown by their absence. Blocks are deduplicated by CID. A single new post might only need a few MST node updates and the record itself, resulting in a CAR slice of a few hundred bytes. The since field in commit events is the Lamport clock synchronization tool: “this diff advances you from rev X to rev Y.”
CIDs act as both the index and the integrity check for CAR blocks. If a block is corrupted or tampered with, it will have a CID that does not match the one claimed in the file. Deduplication is automatic: two blocks with the same CID are guaranteed to be byte-for-byte identical. CAR files are self-verifying and do not require external trust.
CARv1 spec: https://ipld.io/specs/transport/car/carv1/
com.atproto.sync.getRepolexicon: https://lexicon.garden/nsid/com.atproto.sync.getRepoJetstream (lightweight firehose alternative): https://github.com/bluesky-social/jetstream
Closing
CIDs form the backbone of ATProtocol’s core guarantees: identity and data mobility and ownership. Every data assurance the protocol provides is built on these 36 bytes of content-addressed computation.
The evolution of the ATProtocol spec is grounded on lessons from distributed systems, cryptography, existing content-addressed networks like IPFS, and the practical needs of data portability. Each refinement is shaped by both the challenges of real-world deployment and the community of developers and researchers who contribute ideas and critiques. Understanding the influences behind these decisions provides deeper insight into how ATProtocol works, why its verification, synchronization, and portability guarantees matter, and how they may evolve in the future.
Join the discussion on the ATProtocol Community Discourse or in the atmosphere.
Except where otherwise noted, this content is licensed under a Creative Commons Attribution-ShareAlike 4.0 International license with attribution going to Nick Gerakines.