# ATFISL — Content Discovery with the AT Protocol

ATFISL is a content discovery system for CIDs built with the AT Protocol. Publishers announce download locations for CIDs by publishing records to their AT Protocol repositories. Indexers aggregate these records into a lookup table from CIDs to live locations. ATFISL is complemented by FISL which defines an HTTP query API for ATFISL records.

## Introduction

Content-addressed resources are self-certifying: a client that knows a CID can verify the bytes it receives, no matter who serves them (\[[ipfs-principles](#ref-ipfs-principles)\]). This leaves one problem open: given a CID, where do you fetch it from?

RASL (\[[rasl](#ref-rasl)\]) answers this with inline hints, but hints are static: they are baked into the URL at the time of writing and go stale. ATFISL makes discovery dynamic. A publisher announces "this CID is retrievable at these URLs, until this time" and can update or retract the announcement at any moment.

ATFISL uses the AT Protocol (\[[at](#ref-at)\]) as its publication layer rather than defining its own. This is a deliberate trade:

-   Every location record is signed as part of a repository commit, so announcements are attributable to a DID without any new identity or signature scheme.
-   Records live in the publisher's own PDS. Publishers keep credible exit: they can move hosts and their announcements move with them.
-   Indexers remain decoupled with no write API, they observe the network and index what they see.

An indexer can be either scoped or global. You run one for your own data consortium, mirror network, or application, and configure it with the DIDs or collections it should track.

## Location Records

A location record announces that one CID is retrievable at one or more URLs. Records use the `dev.fisl.at` collection in the publisher's repository, keyed by the announced CID.

```
{
  "$type": "dev.fisl.at",
  "addrs": [
    { "url": "https://berjon.com/.well-known/rasl/bafkreifn5yxi7nkftsn46b6x26grda57ict7md2xuvfbsgkiahe2e7vnq4" },
    { "url": "https://mirror.example.com/kitten.jpg", "expires": "2026-09-01T00:00:00Z" }
  ],
  "size": 94201,
  "expires": "2026-09-18T00:00:00Z"
}
```

The fields are:

-   `addrs` (required): an array of one or more addr objects, each describing one location for the content.
-   `size` (optional): the size of the content in bytes, as an integer. This is a hint for clients to plan retrieval; it is not verified by indexers.
-   `expires` (optional): the time after which this record is no longer valid, as an AT Protocol Lexicon <dfn id="dfn-datetime">datetime</dfn> string, which should meet the intersecting requirements of the RFC 3339, ISO 8601, and WHATWG HTML datetime standards (\[[lexicon](#ref-lexicon)\]). Publishers that want to keep content discoverable re-publish records before they expire.

Each addr object has the fields:

-   `url` (required): an absolute URL (\[[url](#ref-url)\]) from which the content can be retrieved. The scheme identifies the retrieval method, and with it how a client turns the URL and the CID into a request; there is no separate protocol field. Schemes may be introduced without a registry, and clients must skip URLs whose schemes they do not support. FISL (\[[fisl](#ref-fisl)\]) specifies retrieval.
-   `expires` (optional): the time after which this addr is no longer valid, as a [datetime](#dfn-datetime) string.

The record's `expires` is a ceiling: the <dfn id="dfn-effective-expiry">effective expiry</dfn> of an addr is the earliest of its own `expires` and the record's, whichever of the two are present. An absent `expires` states no expiry; it does not mean that the addr or the record is expired. An addr past its [effective expiry](#dfn-effective-expiry) is treated as absent. This lets a record mix locations with different lifetimes — a stable mirror next to a short-lived one — without re-publishing the record on the shortest lifetime's cadence. Expiry exists because retrieval locations rot: domains lapse, certificates expire, servers move.

Records and addr objects may carry additional fields, for example to signal capabilities of an endpoint that the scheme alone cannot express. Indexers must preserve fields they do not recognize and clients must ignore them.

The record key is the string form of the announced CID. It must be a valid DASL CID (\[[cid](#ref-cid)\]) or BDASL CID (\[[bdasl](#ref-bdasl)\]); no other CID formats are supported. The record carries no CID field: the key alone identifies the content. This gives each repository at most one live record per CID, which fixes the meaning of every repository operation: a put creates or replaces the announcement, and a delete retracts it. There is no separate update or revocation mechanism, and no way for a repository to hold two records that disagree about the same CID. A DASL CID is a conformant record key with no escaping or transformation (\[[record-key](#ref-record-key)\]): its string form is 59 characters of lowercase base32 including a `b` prefix, so it falls inside the permitted character set, well under the 512 character limit, and free of the case-sensitivity hazard that record keys otherwise carry. A BDASL CID has the same string form, because BDASL changes only the hash type byte (\[[bdasl](#ref-bdasl)\]).

The key also makes a publisher's PDS a discovery endpoint on its own: `getRecord` with the CID as the record key answers "does this DID serve this CID?" in one request, with no indexer (\[[repository](#ref-repository)\]).

Publishers pay for this in two ways. To add or remove one addr, you must read the record, edit `addrs`, and put it back. Use the `swapRecord` parameter of `putRecord` to make the put conditional on the record you read, and retry if it conflicts. And two independent writers cannot announce the same CID side by side — each rewrite replaces the other's addrs. Treat one CID in one repository as owned by one writer. Publishers that refresh many records at once should batch them into one commit with `applyWrites`.

### Lexicon

The Lexicon definition (\[[lexicon](#ref-lexicon)\]) for location records is:

```
{
  "lexicon": 1,
  "id": "dev.fisl.at",
  "defs": {
    "main": {
      "type": "record",
      "description": "An announcement that the content identified by the DASL or BDASL CID in the record key is retrievable at one or more URLs, until an expiry time.",
      "key": "any",
      "record": {
        "type": "object",
        "required": ["addrs"],
        "properties": {
          "addrs": {
            "type": "array",
            "description": "Locations that serve the content.",
            "items": { "type": "ref", "ref": "#addr" },
            "minLength": 1,
            "maxLength": 32
          },
          "size": {
            "type": "integer",
            "description": "Size of the content in bytes.",
            "minimum": 0
          },
          "expires": {
            "type": "string",
            "format": "datetime",
            "description": "Time after which this record is no longer valid. Ceiling for the expiry of every addr."
          }
        }
      }
    },
    "addr": {
      "type": "object",
      "description": "One location that serves the content.",
      "required": ["url"],
      "properties": {
        "url": {
          "type": "string",
          "format": "uri",
          "description": "Absolute URL for the content. The scheme identifies the retrieval method.",
          "maxLength": 2048
        },
        "expires": {
          "type": "string",
          "format": "datetime",
          "description": "Time after which this addr is no longer valid. Defaults to, and is capped by, the record's expires."
        }
      }
    }
  }
}
```

The key type is `any` because Lexicon has no pattern-based key type: of `tid`, `nsid`, `literal:<value>`, and `any`, only `any` admits a CID (\[[record-key](#ref-record-key)\]). The constraint that the key is a DASL or BDASL CID can therefore only be stated in prose, as above, and checked by indexers. Likewise, `format: uri` is looser than the normative rules above; the prose governs.

## Indexing

An indexer builds a lookup table from CIDs to live location records. Use the following steps to <dfn id="dfn-index-a-location-record">index a location record</dfn>:

1.  Accept a record from a repository, obtained by subscribing to a firehose or relay for the `dev.fisl.at` collection, by backfilling repositories, or both (\[[at](#ref-at)\]). Configuration of which DIDs or collections to track is up to the indexer.
2.  Verify the record's inclusion in a signed commit of the publisher's repository. Discard the record if verification fails.
3.  Parse the record key per the steps to [parse a string-encoded CID](https://dasl.ing/cid.html#parse-a-string-encoded-cid), accepting the BDASL hash type extension (\[[bdasl](#ref-bdasl)\]). Discard the record if parsing fails.
4.  Discard the record if its `expires` is malformed, or in the past. Discard any entry of `addrs` whose `url` does not parse as a URL (\[[url](#ref-url)\]), whose `expires` is malformed, or whose [effective expiry](#dfn-effective-expiry) is in the past. Do not discard an addr whose scheme the indexer does not recognize. Discard the record if no entries remain.
5.  Store the record, indexed by that CID, together with the publisher's DID, which FISL serves as the record's `publisher` (\[[fisl](#ref-fisl)\]). Replace any record previously stored for the same DID and CID.
6.  On a delete event for a stored record, remove it.

Because a repository holds at most one live record per CID, an indexer needs no more state than a single row per `(did, cid)` pair, and no reconciliation logic: a put is an upsert on that key, a delete is a delete, and expiry of the row is a comparison against the record's `expires`. An indexer that loses its state can rebuild it by replaying repositories from scratch.

Where two events concern the same `(did, cid)`, the one from the later repository commit wins. Commit revisions (`rev`) are TID-format logical clocks that increase monotonically within a repository and sort lexicographically, so the comparison is a string comparison (\[[repository](#ref-repository)\]). An indexer that processes each repository's events in order needs nothing further; one that backfills and follows a firehose concurrently should record the `rev` alongside each row and ignore any event whose `rev` is not greater, so that a late-arriving put cannot resurrect a retracted record.

Indexers store what publishers said, not what is true: they do not fetch the URLs, verify that the content is there, or interpret URL schemes. Verification belongs to the client at retrieval time; that is the point of content addressing. FISL (\[[fisl](#ref-fisl)\]) specifies how clients query the index and verify what they retrieve.

## Security Considerations

-   Location records are claims, not proofs. A publisher can announce URLs it does not control, or that serve wrong bytes. Client-side CID verification makes this a denial-of-service vector, not an integrity failure.
-   `size` is unverified. Clients must not allocate resources based on it without bounds.

## Open questions

-   This spec is pretty vague about more complex transfer protocols like iroh-blobs or a BAO based HTTP, or even UnixFS with IPFS trustless gateways. One additional protocol, e.g. iroh-blobs, should be explored to ensure protocol flexibility and to avoid overskewing the design for one protocol.

## References

<dfn id="ref-at">\[at\]</dfn>

[AT Protocol](https://atproto.com/specs/atp). URL: [https://atproto.com/](https://atproto.com/)

<dfn id="ref-bdasl">\[bdasl\]</dfn>

Robin Berjon, Brendan O'Brien, & Juan Caballero. [Big DASL (BDASL)](https://dasl.ing/bdasl.html). 2026-10-01. URL: [https://dasl.ing/bdasl.html](https://dasl.ing/bdasl.html)

<dfn id="ref-cid">\[cid\]</dfn>

Robin Berjon & Juan Caballero. [Content IDs (CIDs)](https://dasl.ing/cid.html). 2026-10-01. URL: [https://dasl.ing/cid.html](https://dasl.ing/cid.html)

<dfn id="ref-fisl">\[fisl\]</dfn>

Daniel Norman. [FISL — Finding Internet Structures & Links](https://dasl.ing/fisl.html). 2026-10-01. URL: [https://dasl.ing/fisl.html](https://dasl.ing/fisl.html)

<dfn id="ref-ipfs-principles">\[ipfs-principles\]</dfn>

Robin Berjon. [IPFS Principles](https://specs.ipfs.tech/architecture/principles/). march 2023. URL: [https://specs.ipfs.tech/architecture/principles/](https://specs.ipfs.tech/architecture/principles/)

<dfn id="ref-lexicon">\[lexicon\]</dfn>

[AT Protocol: Lexicon](https://atproto.com/specs/lexicon).

<dfn id="ref-rasl">\[rasl\]</dfn>

Robin Berjon & Juan Caballero. [RASL — Retrieval of Arbitrary Structures & Links](https://dasl.ing/rasl.html). 2026-10-01. URL: [https://dasl.ing/rasl.html](https://dasl.ing/rasl.html)

<dfn id="ref-record-key">\[record-key\]</dfn>

[AT Protocol: Record Key](https://atproto.com/specs/record-key). URL: [https://atproto.com/specs/record-key](https://atproto.com/specs/record-key)

<dfn id="ref-repository">\[repository\]</dfn>

[AT Protocol: Repository](https://atproto.com/specs/repository). URL: [https://atproto.com/specs/repository](https://atproto.com/specs/repository)

<dfn id="ref-url">\[url\]</dfn>

WHATWG. [URL](https://url.spec.whatwg.org/). Living Standard. URL: [https://url.spec.whatwg.org/](https://url.spec.whatwg.org/)