Repository: bluesky-social/indigo

Issue:
new relay seq reset bug
The new relay code (`cmd/relay`) has a feature which allows the sequence number ("cursor") to have an initial value other than 1. For example, you can start at a couple billion.

We set up some new relay instances earlier this year, and set the starting sequence high (like 20 billion). That worked fine, but at some later point the sequence reset to ~zero (aka, to one). It is unclear what caused this: most likely a bug in the relay code, but conceivably could have involved an operational or database issue as well. This happened to 2x relay instances around the same time, which means a code bug is much more likely.

Things to look at:

- 32-bit vs 64-bit integers (seq was set to 20 billion and is now under 4 billion)
- behavior on startup if database is not available
- persistence roll-over logic
- seq handling code in general

Discussion:

Solution patch:
diff --git a/.github/workflows/container-relay-ghcr.yaml b/.github/workflows/container-relay-ghcr.yaml
new file mode 100644
index 00000000..6e5fcb4c
--- /dev/null
+++ b/.github/workflows/container-relay-ghcr.yaml
@@ -0,0 +1,53 @@
+name: container-relay-ghcr
+on: [push]
+env:
+  REGISTRY: ghcr.io
+  USERNAME: ${{ github.actor }}
+  PASSWORD: ${{ secrets.GITHUB_TOKEN }}
+
+  # github.repository as <account>/<repo>
+  IMAGE_NAME: ${{ github.repository }}
+
+jobs:
+  container-bigsky-ghcr:
+    if: github.repository == 'bluesky-social/indigo'
+    runs-on: ubuntu-latest
+    permissions:
+      contents: read
+      packages: write
+      id-token: write
+
+    steps:
+      - name: Checkout repository
+        uses: actions/checkout@v3
+
+      - name: Setup Docker buildx
+        uses: docker/setup-buildx-action@v1
+
+      - name: Log into registry ${{ env.REGISTRY }}
+        uses: docker/login-action@v2
+        with:
+          registry: ${{ env.REGISTRY }}
+          username: ${{ env.USERNAME }}
+          password: ${{ env.PASSWORD }}
+
+      - name: Extract Docker metadata
+        id: meta
+        uses: docker/metadata-action@v4
+        with:
+          images: |
+            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
+          tags: |
+            type=sha,enable=true,priority=100,prefix=relay:,suffix=,format=long
+
+      - name: Build and push Docker image
+        id: build-and-push
+        uses: docker/build-push-action@v4
+        with:
+          context: .
+          file: ./cmd/relay/Dockerfile
+          push: ${{ github.event_name != 'pull_request' }}
+          tags: ${{ steps.meta.outputs.tags }}
+          labels: ${{ steps.meta.outputs.labels }}
+          cache-from: type=gha
+          cache-to: type=gha,mode=max
diff --git a/.github/workflows/container-sonar-ghcr.yaml b/.github/workflows/container-sonar-ghcr.yaml
new file mode 100644
index 00000000..22e0eefd
--- /dev/null
+++ b/.github/workflows/container-sonar-ghcr.yaml
@@ -0,0 +1,53 @@
+name: container-sonar-ghcr
+on: [push]
+env:
+  REGISTRY: ghcr.io
+  USERNAME: ${{ github.actor }}
+  PASSWORD: ${{ secrets.GITHUB_TOKEN }}
+
+  # github.repository as <account>/<repo>
+  IMAGE_NAME: ${{ github.repository }}
+
+jobs:
+  container-bigsky-ghcr:
+    if: github.repository == 'bluesky-social/indigo'
+    runs-on: ubuntu-latest
+    permissions:
+      contents: read
+      packages: write
+      id-token: write
+
+    steps:
+      - name: Checkout repository
+        uses: actions/checkout@v3
+
+      - name: Setup Docker buildx
+        uses: docker/setup-buildx-action@v1
+
+      - name: Log into registry ${{ env.REGISTRY }}
+        uses: docker/login-action@v2
+        with:
+          registry: ${{ env.REGISTRY }}
+          username: ${{ env.USERNAME }}
+          password: ${{ env.PASSWORD }}
+
+      - name: Extract Docker metadata
+        id: meta
+        uses: docker/metadata-action@v4
+        with:
+          images: |
+            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
+          tags: |
+            type=sha,enable=true,priority=100,prefix=sonar:,suffix=,format=long
+
+      - name: Build and push Docker image
+        id: build-and-push
+        uses: docker/build-push-action@v4
+        with:
+          context: .
+          file: ./cmd/sonar/Dockerfile
+          push: ${{ github.event_name != 'pull_request' }}
+          tags: ${{ steps.meta.outputs.tags }}
+          labels: ${{ steps.meta.outputs.labels }}
+          cache-from: type=gha
+          cache-to: type=gha,mode=max
diff --git a/.github/workflows/golang.yml b/.github/workflows/golang.yml
index 29f19677..fe4289fd 100644
--- a/.github/workflows/golang.yml
+++ b/.github/workflows/golang.yml
@@ -19,7 +19,7 @@ jobs:
       - name: Set up Go tooling
         uses: actions/setup-go@v4
         with:
-          go-version: "1.24"
+          go-version: "1.25"
       - name: Build
         run: make build
       - name: Test
@@ -32,6 +32,6 @@ jobs:
       - name: Set up Go tooling
         uses: actions/setup-go@v4
         with:
-          go-version: "1.24"
+          go-version: "1.25"
       - name: Lint
         run: make lint
diff --git a/Makefile b/Makefile
index 7d506f30..9235c04b 100644
--- a/Makefile
+++ b/Makefile
@@ -3,7 +3,7 @@ SHELL = /bin/bash
 .SHELLFLAGS = -o pipefail -c
 
 # base path for Lexicon document tree (for lexgen)
-LEXDIR?=../atproto/lexicons
+LEXDIR?=./lexicons
 
 # https://github.com/golang/go/wiki/LoopvarExperiment
 export GOEXPERIMENT := loopvar
diff --git a/README.md b/README.md
index 88ad2cd8..0b6283e1 100644
--- a/README.md
+++ b/README.md
@@ -25,7 +25,7 @@ Some Bluesky software is developed in Typescript, and lives in the [bluesky-soci
 | ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
 | `api/atproto`: generated types for `com.atproto.*` Lexicons  | [![PkgGoDev](https://pkg.go.dev/badge/mod/github.com/bluesky-social/indigo/api/atproto)](https://pkg.go.dev/mod/github.com/bluesky-social/indigo/api/atproto)           |
 | `api/bsky`: generated types for `app.bsky.*` Lexicons        | [![PkgGoDev](https://pkg.go.dev/badge/mod/github.com/bluesky-social/indigo/api/bsky)](https://pkg.go.dev/mod/github.com/bluesky-social/indigo/api/bsky)                 |
-| `atproto/atcrypto`: crytographic signing and key serialization | [![PkgGoDev](https://pkg.go.dev/badge/mod/github.com/bluesky-social/indigo/atproto/atcrypto)](https://pkg.go.dev/mod/github.com/bluesky-social/indigo/atproto/atcrypto)     |
+| `atproto/atcrypto`: cryptographic signing and key serialization | [![PkgGoDev](https://pkg.go.dev/badge/mod/github.com/bluesky-social/indigo/atproto/atcrypto)](https://pkg.go.dev/mod/github.com/bluesky-social/indigo/atproto/atcrypto)     |
 | `atproto/identity`: DID and handle resolution                | [![PkgGoDev](https://pkg.go.dev/badge/mod/github.com/bluesky-social/indigo/atproto/identity)](https://pkg.go.dev/mod/github.com/bluesky-social/indigo/atproto/identity) |
 | `atproto/syntax`: string types and parsers for identifiers   | [![PkgGoDev](https://pkg.go.dev/badge/mod/github.com/bluesky-social/indigo/atproto/syntax)](https://pkg.go.dev/mod/github.com/bluesky-social/indigo/atproto/syntax)     |
 | `atproto/lexicon`: schema validation of data                 | [![PkgGoDev](https://pkg.go.dev/badge/mod/github.com/bluesky-social/indigo/atproto/lexicon)](https://pkg.go.dev/mod/github.com/bluesky-social/indigo/atproto/lexicon)     |
diff --git a/api/bsky/actordefs.go b/api/bsky/actordefs.go
index 0d9be348..51b9881b 100644
--- a/api/bsky/actordefs.go
+++ b/api/bsky/actordefs.go
@@ -505,8 +505,6 @@ func (t *ActorDefs_StatusView_Embed) UnmarshalJSON(b []byte) error {
 // ActorDefs_ThreadViewPref is a "threadViewPref" in the app.bsky.actor.defs schema.
 type ActorDefs_ThreadViewPref struct {
 	LexiconTypeID string `json:"$type" cborgen:"$type,const=app.bsky.actor.defs#threadViewPref"`
-	// prioritizeFollowedUsers: Show followed users at the top of all replies.
-	PrioritizeFollowedUsers *bool `json:"prioritizeFollowedUsers,omitempty" cborgen:"prioritizeFollowedUsers,omitempty"`
 	// sort: Sorting mode for threads.
 	Sort *string `json:"sort,omitempty" cborgen:"sort,omitempty"`
 }
diff --git a/api/bsky/ageassurancebegin.go b/api/bsky/ageassurancebegin.go
new file mode 100644
index 00000000..d6bb07f5
--- /dev/null
+++ b/api/bsky/ageassurancebegin.go
@@ -0,0 +1,33 @@
+// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT.
+
+// Lexicon schema: app.bsky.ageassurance.begin
+
+package bsky
+
+import (
+	"context"
+
+	lexutil "github.com/bluesky-social/indigo/lex/util"
+)
+
+// AgeassuranceBegin_Input is the input argument to a app.bsky.ageassurance.begin call.
+type AgeassuranceBegin_Input struct {
+	// countryCode: An ISO 3166-1 alpha-2 code of the user's location.
+	CountryCode string `json:"countryCode" cborgen:"countryCode"`
+	// email: The user's email address to receive Age Assurance instructions.
+	Email string `json:"email" cborgen:"email"`
+	// language: The user's preferred language for communication during the Age Assurance process.
+	Language string `json:"language" cborgen:"language"`
+	// regionCode: An optional ISO 3166-2 code of the user's region or state within the country.
+	RegionCode *string `json:"regionCode,omitempty" cborgen:"regionCode,omitempty"`
+}
+
+// AgeassuranceBegin calls the XRPC method "app.bsky.ageassurance.begin".
+func AgeassuranceBegin(ctx context.Context, c lexutil.LexClient, input *AgeassuranceBegin_Input) (*AgeassuranceDefs_State, error) {
+	var out AgeassuranceDefs_State
+	if err := c.LexDo(ctx, lexutil.Procedure, "application/json", "app.bsky.ageassurance.begin", nil, input, &out); err != nil {
+		return nil, err
+	}
+
+	return &out, nil
+}
diff --git a/api/bsky/ageassurancedefs.go b/api/bsky/ageassurancedefs.go
new file mode 100644
index 00000000..524088b9
--- /dev/null
+++ b/api/bsky/ageassurancedefs.go
@@ -0,0 +1,219 @@
+// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT.
+
+// Lexicon schema: app.bsky.ageassurance.defs
+
+package bsky
+
+import (
+	"encoding/json"
+	"fmt"
+
+	lexutil "github.com/bluesky-social/indigo/lex/util"
+)
+
+// AgeassuranceDefs_Config is a "config" in the app.bsky.ageassurance.defs schema.
+type AgeassuranceDefs_Config struct {
+	// regions: The per-region Age Assurance configuration.
+	Regions []*AgeassuranceDefs_ConfigRegion `json:"regions" cborgen:"regions"`
+}
+
+// AgeassuranceDefs_ConfigRegion is a "configRegion" in the app.bsky.ageassurance.defs schema.
+//
+// The Age Assurance configuration for a specific region.
+type AgeassuranceDefs_ConfigRegion struct {
+	// countryCode: The ISO 3166-1 alpha-2 country code this configuration applies to.
+	CountryCode string `json:"countryCode" cborgen:"countryCode"`
+	// regionCode: The ISO 3166-2 region code this configuration applies to. If omitted, the configuration applies to the entire country.
+	RegionCode *string `json:"regionCode,omitempty" cborgen:"regionCode,omitempty"`
+	// rules: The ordered list of Age Assurance rules that apply to this region. Rules should be applied in order, and the first matching rule determines the access level granted. The rules array should always include a default rule as the last item.
+	Rules []*AgeassuranceDefs_ConfigRegion_Rules_Elem `json:"rules" cborgen:"rules"`
+}
+
+// AgeassuranceDefs_ConfigRegionRuleDefault is a "configRegionRuleDefault" in the app.bsky.ageassurance.defs schema.
+//
+// Age Assurance rule that applies by default.
+type AgeassuranceDefs_ConfigRegionRuleDefault struct {
+	LexiconTypeID string  `json:"$type" cborgen:"$type,const=app.bsky.ageassurance.defs#configRegionRuleDefault"`
+	Access        *string `json:"access" cborgen:"access"`
+}
+
+// AgeassuranceDefs_ConfigRegionRuleIfAccountNewerThan is a "configRegionRuleIfAccountNewerThan" in the app.bsky.ageassurance.defs schema.
+//
+// Age Assurance rule that applies if the account is equal-to or newer than a certain date.
+type AgeassuranceDefs_ConfigRegionRuleIfAccountNewerThan struct {
+	LexiconTypeID string  `json:"$type" cborgen:"$type,const=app.bsky.ageassurance.defs#configRegionRuleIfAccountNewerThan"`
+	Access        *string `json:"access" cborgen:"access"`
+	// date: The date threshold as a datetime string.
+	Date string `json:"date" cborgen:"date"`
+}
+
+// AgeassuranceDefs_ConfigRegionRuleIfAccountOlderThan is a "configRegionRuleIfAccountOlderThan" in the app.bsky.ageassurance.defs schema.
+//
+// Age Assurance rule that applies if the account is older than a certain date.
+type AgeassuranceDefs_ConfigRegionRuleIfAccountOlderThan struct {
+	LexiconTypeID string  `json:"$type" cborgen:"$type,const=app.bsky.ageassurance.defs#configRegionRuleIfAccountOlderThan"`
+	Access        *string `json:"access" cborgen:"access"`
+	// date: The date threshold as a datetime string.
+	Date string `json:"date" cborgen:"date"`
+}
+
+// AgeassuranceDefs_ConfigRegionRuleIfAssuredOverAge is a "configRegionRuleIfAssuredOverAge" in the app.bsky.ageassurance.defs schema.
+//
+// Age Assurance rule that applies if the user has been assured to be equal-to or over a certain age.
+type AgeassuranceDefs_ConfigRegionRuleIfAssuredOverAge struct {
+	LexiconTypeID string  `json:"$type" cborgen:"$type,const=app.bsky.ageassurance.defs#configRegionRuleIfAssuredOverAge"`
+	Access        *string `json:"access" cborgen:"access"`
+	// age: The age threshold as a whole integer.
+	Age int64 `json:"age" cborgen:"age"`
+}
+
+// AgeassuranceDefs_ConfigRegionRuleIfAssuredUnderAge is a "configRegionRuleIfAssuredUnderAge" in the app.bsky.ageassurance.defs schema.
+//
+// Age Assurance rule that applies if the user has been assured to be under a certain age.
+type AgeassuranceDefs_ConfigRegionRuleIfAssuredUnderAge struct {
+	LexiconTypeID string  `json:"$type" cborgen:"$type,const=app.bsky.ageassurance.defs#configRegionRuleIfAssuredUnderAge"`
+	Access        *string `json:"access" cborgen:"access"`
+	// age: The age threshold as a whole integer.
+	Age int64 `json:"age" cborgen:"age"`
+}
+
+// AgeassuranceDefs_ConfigRegionRuleIfDeclaredOverAge is a "configRegionRuleIfDeclaredOverAge" in the app.bsky.ageassurance.defs schema.
+//
+// Age Assurance rule that applies if the user has declared themselves equal-to or over a certain age.
+type AgeassuranceDefs_ConfigRegionRuleIfDeclaredOverAge struct {
+	LexiconTypeID string  `json:"$type" cborgen:"$type,const=app.bsky.ageassurance.defs#configRegionRuleIfDeclaredOverAge"`
+	Access        *string `json:"access" cborgen:"access"`
+	// age: The age threshold as a whole integer.
+	Age int64 `json:"age" cborgen:"age"`
+}
+
+// AgeassuranceDefs_ConfigRegionRuleIfDeclaredUnderAge is a "configRegionRuleIfDeclaredUnderAge" in the app.bsky.ageassurance.defs schema.
+//
+// Age Assurance rule that applies if the user has declared themselves under a certain age.
+type AgeassuranceDefs_ConfigRegionRuleIfDeclaredUnderAge struct {
+	LexiconTypeID string  `json:"$type" cborgen:"$type,const=app.bsky.ageassurance.defs#configRegionRuleIfDeclaredUnderAge"`
+	Access        *string `json:"access" cborgen:"access"`
+	// age: The age threshold as a whole integer.
+	Age int64 `json:"age" cborgen:"age"`
+}
+
+type AgeassuranceDefs_ConfigRegion_Rules_Elem struct {
+	AgeassuranceDefs_ConfigRegionRuleDefault            *AgeassuranceDefs_ConfigRegionRuleDefault
+	AgeassuranceDefs_ConfigRegionRuleIfDeclaredOverAge  *AgeassuranceDefs_ConfigRegionRuleIfDeclaredOverAge
+	AgeassuranceDefs_ConfigRegionRuleIfDeclaredUnderAge *AgeassuranceDefs_ConfigRegionRuleIfDeclaredUnderAge
+	AgeassuranceDefs_ConfigRegionRuleIfAssuredOverAge   *AgeassuranceDefs_ConfigRegionRuleIfAssuredOverAge
+	AgeassuranceDefs_ConfigRegionRuleIfAssuredUnderAge  *AgeassuranceDefs_ConfigRegionRuleIfAssuredUnderAge
+	AgeassuranceDefs_ConfigRegionRuleIfAccountNewerThan *AgeassuranceDefs_ConfigRegionRuleIfAccountNewerThan
+	AgeassuranceDefs_ConfigRegionRuleIfAccountOlderThan *AgeassuranceDefs_ConfigRegionRuleIfAccountOlderThan
+}
+
+func (t *AgeassuranceDefs_ConfigRegion_Rules_Elem) MarshalJSON() ([]byte, error) {
+	if t.AgeassuranceDefs_ConfigRegionRuleDefault != nil {
+		t.AgeassuranceDefs_ConfigRegionRuleDefault.LexiconTypeID = "app.bsky.ageassurance.defs#configRegionRuleDefault"
+		return json.Marshal(t.AgeassuranceDefs_ConfigRegionRuleDefault)
+	}
+	if t.AgeassuranceDefs_ConfigRegionRuleIfDeclaredOverAge != nil {
+		t.AgeassuranceDefs_ConfigRegionRuleIfDeclaredOverAge.LexiconTypeID = "app.bsky.ageassurance.defs#configRegionRuleIfDeclaredOverAge"
+		return json.Marshal(t.AgeassuranceDefs_ConfigRegionRuleIfDeclaredOverAge)
+	}
+	if t.AgeassuranceDefs_ConfigRegionRuleIfDeclaredUnderAge != nil {
+		t.AgeassuranceDefs_ConfigRegionRuleIfDeclaredUnderAge.LexiconTypeID = "app.bsky.ageassurance.defs#configRegionRuleIfDeclaredUnderAge"
+		return json.Marshal(t.AgeassuranceDefs_ConfigRegionRuleIfDeclaredUnderAge)
+	}
+	if t.AgeassuranceDefs_ConfigRegionRuleIfAssuredOverAge != nil {
+		t.AgeassuranceDefs_ConfigRegionRuleIfAssuredOverAge.LexiconTypeID = "app.bsky.ageassurance.defs#configRegionRuleIfAssuredOverAge"
+		return json.Marshal(t.AgeassuranceDefs_ConfigRegionRuleIfAssuredOverAge)
+	}
+	if t.AgeassuranceDefs_ConfigRegionRuleIfAssuredUnderAge != nil {
+		t.AgeassuranceDefs_ConfigRegionRuleIfAssuredUnderAge.LexiconTypeID = "app.bsky.ageassurance.defs#configRegionRuleIfAssuredUnderAge"
+		return json.Marshal(t.AgeassuranceDefs_ConfigRegionRuleIfAssuredUnderAge)
+	}
+	if t.AgeassuranceDefs_ConfigRegionRuleIfAccountNewerThan != nil {
+		t.AgeassuranceDefs_ConfigRegionRuleIfAccountNewerThan.LexiconTypeID = "app.bsky.ageassurance.defs#configRegionRuleIfAccountNewerThan"
+		return json.Marshal(t.AgeassuranceDefs_ConfigRegionRuleIfAccountNewerThan)
+	}
+	if t.AgeassuranceDefs_ConfigRegionRuleIfAccountOlderThan != nil {
+		t.AgeassuranceDefs_ConfigRegionRuleIfAccountOlderThan.LexiconTypeID = "app.bsky.ageassurance.defs#configRegionRuleIfAccountOlderThan"
+		return json.Marshal(t.AgeassuranceDefs_ConfigRegionRuleIfAccountOlderThan)
+	}
+	return nil, fmt.Errorf("can not marshal empty union as JSON")
+}
+
+func (t *AgeassuranceDefs_ConfigRegion_Rules_Elem) UnmarshalJSON(b []byte) error {
+	typ, err := lexutil.TypeExtract(b)
+	if err != nil {
+		return err
+	}
+
+	switch typ {
+	case "app.bsky.ageassurance.defs#configRegionRuleDefault":
+		t.AgeassuranceDefs_ConfigRegionRuleDefault = new(AgeassuranceDefs_ConfigRegionRuleDefault)
+		return json.Unmarshal(b, t.AgeassuranceDefs_ConfigRegionRuleDefault)
+	case "app.bsky.ageassurance.defs#configRegionRuleIfDeclaredOverAge":
+		t.AgeassuranceDefs_ConfigRegionRuleIfDeclaredOverAge = new(AgeassuranceDefs_ConfigRegionRuleIfDeclaredOverAge)
+		return json.Unmarshal(b, t.AgeassuranceDefs_ConfigRegionRuleIfDeclaredOverAge)
+	case "app.bsky.ageassurance.defs#configRegionRuleIfDeclaredUnderAge":
+		t.AgeassuranceDefs_ConfigRegionRuleIfDeclaredUnderAge = new(AgeassuranceDefs_ConfigRegionRuleIfDeclaredUnderAge)
+		return json.Unmarshal(b, t.AgeassuranceDefs_ConfigRegionRuleIfDeclaredUnderAge)
+	case "app.bsky.ageassurance.defs#configRegionRuleIfAssuredOverAge":
+		t.AgeassuranceDefs_ConfigRegionRuleIfAssuredOverAge = new(AgeassuranceDefs_ConfigRegionRuleIfAssuredOverAge)
+		return json.Unmarshal(b, t.AgeassuranceDefs_ConfigRegionRuleIfAssuredOverAge)
+	case "app.bsky.ageassurance.defs#configRegionRuleIfAssuredUnderAge":
+		t.AgeassuranceDefs_ConfigRegionRuleIfAssuredUnderAge = new(AgeassuranceDefs_ConfigRegionRuleIfAssuredUnderAge)
+		return json.Unmarshal(b, t.AgeassuranceDefs_ConfigRegionRuleIfAssuredUnderAge)
+	case "app.bsky.ageassurance.defs#configRegionRuleIfAccountNewerThan":
+		t.AgeassuranceDefs_ConfigRegionRuleIfAccountNewerThan = new(AgeassuranceDefs_ConfigRegionRuleIfAccountNewerThan)
+		return json.Unmarshal(b, t.AgeassuranceDefs_ConfigRegionRuleIfAccountNewerThan)
+	case "app.bsky.ageassurance.defs#configRegionRuleIfAccountOlderThan":
+		t.AgeassuranceDefs_ConfigRegionRuleIfAccountOlderThan = new(AgeassuranceDefs_ConfigRegionRuleIfAccountOlderThan)
+		return json.Unmarshal(b, t.AgeassuranceDefs_ConfigRegionRuleIfAccountOlderThan)
+	default:
+		return nil
+	}
+}
+
+// AgeassuranceDefs_Event is a "event" in the app.bsky.ageassurance.defs schema.
+//
+// Object used to store Age Assurance data in stash.
+type AgeassuranceDefs_Event struct {
+	// access: The access level granted based on Age Assurance data we've processed.
+	Access string `json:"access" cborgen:"access"`
+	// attemptId: The unique identifier for this instance of the Age Assurance flow, in UUID format.
+	AttemptId string `json:"attemptId" cborgen:"attemptId"`
+	// completeIp: The IP address used when completing the Age Assurance flow.
+	CompleteIp *string `json:"completeIp,omitempty" cborgen:"completeIp,omitempty"`
+	// completeUa: The user agent used when completing the Age Assurance flow.
+	CompleteUa *string `json:"completeUa,omitempty" cborgen:"completeUa,omitempty"`
+	// countryCode: The ISO 3166-1 alpha-2 country code provided when beginning the Age Assurance flow.
+	CountryCode string `json:"countryCode" cborgen:"countryCode"`
+	// createdAt: The date and time of this write operation.
+	CreatedAt string `json:"createdAt" cborgen:"createdAt"`
+	// email: The email used for Age Assurance.
+	Email *string `json:"email,omitempty" cborgen:"email,omitempty"`
+	// initIp: The IP address used when initiating the Age Assurance flow.
+	InitIp *string `json:"initIp,omitempty" cborgen:"initIp,omitempty"`
+	// initUa: The user agent used when initiating the Age Assurance flow.
+	InitUa *string `json:"initUa,omitempty" cborgen:"initUa,omitempty"`
+	// regionCode: The ISO 3166-2 region code provided when beginning the Age Assurance flow.
+	RegionCode *string `json:"regionCode,omitempty" cborgen:"regionCode,omitempty"`
+	// status: The status of the Age Assurance process.
+	Status string `json:"status" cborgen:"status"`
+}
+
+// AgeassuranceDefs_State is a "state" in the app.bsky.ageassurance.defs schema.
+//
+// The user's computed Age Assurance state.
+type AgeassuranceDefs_State struct {
+	Access *string `json:"access" cborgen:"access"`
+	// lastInitiatedAt: The timestamp when this state was last updated.
+	LastInitiatedAt *string `json:"lastInitiatedAt,omitempty" cborgen:"lastInitiatedAt,omitempty"`
+	Status          *string `json:"status" cborgen:"status"`
+}
+
+// AgeassuranceDefs_StateMetadata is a "stateMetadata" in the app.bsky.ageassurance.defs schema.
+//
+// Additional metadata needed to compute Age Assurance state client-side.
+type AgeassuranceDefs_StateMetadata struct {
+	// accountCreatedAt: The account creation timestamp.
+	AccountCreatedAt *string `json:"accountCreatedAt,omitempty" cborgen:"accountCreatedAt,omitempty"`
+}
diff --git a/api/bsky/ageassurancegetConfig.go b/api/bsky/ageassurancegetConfig.go
new file mode 100644
index 00000000..77f50b02
--- /dev/null
+++ b/api/bsky/ageassurancegetConfig.go
@@ -0,0 +1,21 @@
+// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT.
+
+// Lexicon schema: app.bsky.ageassurance.getConfig
+
+package bsky
+
+import (
+	"context"
+
+	lexutil "github.com/bluesky-social/indigo/lex/util"
+)
+
+// AgeassuranceGetConfig calls the XRPC method "app.bsky.ageassurance.getConfig".
+func AgeassuranceGetConfig(ctx context.Context, c lexutil.LexClient) (*AgeassuranceDefs_Config, error) {
+	var out AgeassuranceDefs_Config
+	if err := c.LexDo(ctx, lexutil.Query, "", "app.bsky.ageassurance.getConfig", nil, nil, &out); err != nil {
+		return nil, err
+	}
+
+	return &out, nil
+}
diff --git a/api/bsky/ageassurancegetState.go b/api/bsky/ageassurancegetState.go
new file mode 100644
index 00000000..d6aac8f0
--- /dev/null
+++ b/api/bsky/ageassurancegetState.go
@@ -0,0 +1,33 @@
+// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT.
+
+// Lexicon schema: app.bsky.ageassurance.getState
+
+package bsky
+
+import (
+	"context"
+
+	lexutil "github.com/bluesky-social/indigo/lex/util"
+)
+
+// AgeassuranceGetState_Output is the output of a app.bsky.ageassurance.getState call.
+type AgeassuranceGetState_Output struct {
+	Metadata *AgeassuranceDefs_StateMetadata `json:"metadata" cborgen:"metadata"`
+	State    *AgeassuranceDefs_State         `json:"state" cborgen:"state"`
+}
+
+// AgeassuranceGetState calls the XRPC method "app.bsky.ageassurance.getState".
+func AgeassuranceGetState(ctx context.Context, c lexutil.LexClient, countryCode string, regionCode string) (*AgeassuranceGetState_Output, error) {
+	var out AgeassuranceGetState_Output
+
+	params := map[string]interface{}{}
+	params["countryCode"] = countryCode
+	if regionCode != "" {
+		params["regionCode"] = regionCode
+	}
+	if err := c.LexDo(ctx, lexutil.Query, "", "app.bsky.ageassurance.getState", params, nil, &out); err != nil {
+		return nil, err
+	}
+
+	return &out, nil
+}
diff --git a/api/bsky/cbor_gen.go b/api/bsky/cbor_gen.go
index 0253f8ec..e8de2eda 100644
--- a/api/bsky/cbor_gen.go
+++ b/api/bsky/cbor_gen.go
@@ -2021,11 +2021,35 @@ func (t *GraphFollow) MarshalCBOR(w io.Writer) error {
 	}
 
 	cw := cbg.NewCborWriter(w)
+	fieldCount := 4
 
-	if _, err := cw.Write([]byte{163}); err != nil {
+	if t.Via == nil {
+		fieldCount--
+	}
+
+	if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil {
 		return err
 	}
 
+	// t.Via (atproto.RepoStrongRef) (struct)
+	if t.Via != nil {
+
+		if len("via") > 1000000 {
+			return xerrors.Errorf("Value in field \"via\" was too long")
+		}
+
+		if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("via"))); err != nil {
+			return err
+		}
+		if _, err := cw.WriteString(string("via")); err != nil {
+			return err
+		}
+
+		if err := t.Via.MarshalCBOR(cw); err != nil {
+			return err
+		}
+	}
+
 	// t.LexiconTypeID (string) (string)
 	if len("$type") > 1000000 {
 		return xerrors.Errorf("Value in field \"$type\" was too long")
@@ -2134,7 +2158,27 @@ func (t *GraphFollow) UnmarshalCBOR(r io.Reader) (err error) {
 		}
 
 		switch string(nameBuf[:nameLen]) {
-		// t.LexiconTypeID (string) (string)
+		// t.Via (atproto.RepoStrongRef) (struct)
+		case "via":
+
+			{
+
+				b, err := cr.ReadByte()
+				if err != nil {
+					return err
+				}
+				if b != cbg.CborNull[0] {
+					if err := cr.UnreadByte(); err != nil {
+						return err
+					}
+					t.Via = new(atproto.RepoStrongRef)
+					if err := t.Via.UnmarshalCBOR(cr); err != nil {
+						return xerrors.Errorf("unmarshaling t.Via pointer: %w", err)
+					}
+				}
+
+			}
+			// t.LexiconTypeID (string) (string)
 		case "$type":
 
 			{
diff --git a/api/bsky/unspeccedgetPostThreadOtherV2.go b/api/bsky/unspeccedgetPostThreadOtherV2.go
index 5a61a4ef..e424a814 100644
--- a/api/bsky/unspeccedgetPostThreadOtherV2.go
+++ b/api/bsky/unspeccedgetPostThreadOtherV2.go
@@ -56,15 +56,11 @@ func (t *UnspeccedGetPostThreadOtherV2_ThreadItem_Value) UnmarshalJSON(b []byte)
 // UnspeccedGetPostThreadOtherV2 calls the XRPC method "app.bsky.unspecced.getPostThreadOtherV2".
 //
 // anchor: Reference (AT-URI) to post record. This is the anchor post.
-// prioritizeFollowedUsers: Whether to prioritize posts from followed users. It only has effect when the user is authenticated.
-func UnspeccedGetPostThreadOtherV2(ctx context.Context, c lexutil.LexClient, anchor string, prioritizeFollowedUsers bool) (*UnspeccedGetPostThreadOtherV2_Output, error) {
+func UnspeccedGetPostThreadOtherV2(ctx context.Context, c lexutil.LexClient, anchor string) (*UnspeccedGetPostThreadOtherV2_Output, error) {
 	var out UnspeccedGetPostThreadOtherV2_Output
 
 	params := map[string]interface{}{}
 	params["anchor"] = anchor
-	if prioritizeFollowedUsers {
-		params["prioritizeFollowedUsers"] = prioritizeFollowedUsers
-	}
 	if err := c.LexDo(ctx, lexutil.Query, "", "app.bsky.unspecced.getPostThreadOtherV2", params, nil, &out); err != nil {
 		return nil, err
 	}
diff --git a/api/bsky/unspeccedgetPostThreadV2.go b/api/bsky/unspeccedgetPostThreadV2.go
index f3cd6240..344a0fbf 100644
--- a/api/bsky/unspeccedgetPostThreadV2.go
+++ b/api/bsky/unspeccedgetPostThreadV2.go
@@ -86,9 +86,8 @@ func (t *UnspeccedGetPostThreadV2_ThreadItem_Value) UnmarshalJSON(b []byte) erro
 // anchor: Reference (AT-URI) to post record. This is the anchor post, and the thread will be built around it. It can be any post in the tree, not necessarily a root post.
 // below: How many levels of replies to include below the anchor.
 // branchingFactor: Maximum of replies to include at each level of the thread, except for the direct replies to the anchor, which are (NOTE: currently, during unspecced phase) all returned (NOTE: later they might be paginated).
-// prioritizeFollowedUsers: Whether to prioritize posts from followed users. It only has effect when the user is authenticated.
 // sort: Sorting for the thread replies.
-func UnspeccedGetPostThreadV2(ctx context.Context, c lexutil.LexClient, above bool, anchor string, below int64, branchingFactor int64, prioritizeFollowedUsers bool, sort string) (*UnspeccedGetPostThreadV2_Output, error) {
+func UnspeccedGetPostThreadV2(ctx context.Context, c lexutil.LexClient, above bool, anchor string, below int64, branchingFactor int64, sort string) (*UnspeccedGetPostThreadV2_Output, error) {
 	var out UnspeccedGetPostThreadV2_Output
 
 	params := map[string]interface{}{}
@@ -102,9 +101,6 @@ func UnspeccedGetPostThreadV2(ctx context.Context, c lexutil.LexClient, above bo
 	if branchingFactor != 0 {
 		params["branchingFactor"] = branchingFactor
 	}
-	if prioritizeFollowedUsers {
-		params["prioritizeFollowedUsers"] = prioritizeFollowedUsers
-	}
 	if sort != "" {
 		params["sort"] = sort
 	}
diff --git a/api/ozone/moderationdefs.go b/api/ozone/moderationdefs.go
index 1e23a82f..684e8354 100644
--- a/api/ozone/moderationdefs.go
+++ b/api/ozone/moderationdefs.go
@@ -70,20 +70,25 @@ type ModerationDefs_AccountStrike struct {
 //
 // Age assurance info coming directly from users. Only works on DID subjects.
 type ModerationDefs_AgeAssuranceEvent struct {
-	LexiconTypeID string `json:"$type" cborgen:"$type,const=tools.ozone.moderation.defs#ageAssuranceEvent"`
+	LexiconTypeID string  `json:"$type" cborgen:"$type,const=tools.ozone.moderation.defs#ageAssuranceEvent"`
+	Access        *string `json:"access,omitempty" cborgen:"access,omitempty"`
 	// attemptId: The unique identifier for this instance of the age assurance flow, in UUID format.
 	AttemptId string `json:"attemptId" cborgen:"attemptId"`
 	// completeIp: The IP address used when completing the AA flow.
 	CompleteIp *string `json:"completeIp,omitempty" cborgen:"completeIp,omitempty"`
 	// completeUa: The user agent used when completing the AA flow.
 	CompleteUa *string `json:"completeUa,omitempty" cborgen:"completeUa,omitempty"`
+	// countryCode: The ISO 3166-1 alpha-2 country code provided when beginning the Age Assurance flow.
+	CountryCode *string `json:"countryCode,omitempty" cborgen:"countryCode,omitempty"`
 	// createdAt: The date and time of this write operation.
 	CreatedAt string `json:"createdAt" cborgen:"createdAt"`
 	// initIp: The IP address used when initiating the AA flow.
 	InitIp *string `json:"initIp,omitempty" cborgen:"initIp,omitempty"`
 	// initUa: The user agent used when initiating the AA flow.
 	InitUa *string `json:"initUa,omitempty" cborgen:"initUa,omitempty"`
-	// status: The status of the age assurance process.
+	// regionCode: The ISO 3166-2 region code provided when beginning the Age Assurance flow.
+	RegionCode *string `json:"regionCode,omitempty" cborgen:"regionCode,omitempty"`
+	// status: The status of the Age Assurance process.
 	Status string `json:"status" cborgen:"status"`
 }
 
@@ -91,7 +96,8 @@ type ModerationDefs_AgeAssuranceEvent struct {
 //
 // Age assurance status override by moderators. Only works on DID subjects.
 type ModerationDefs_AgeAssuranceOverrideEvent struct {
-	LexiconTypeID string `json:"$type" cborgen:"$type,const=tools.ozone.moderation.defs#ageAssuranceOverrideEvent"`
+	LexiconTypeID string  `json:"$type" cborgen:"$type,const=tools.ozone.moderation.defs#ageAssuranceOverrideEvent"`
+	Access        *string `json:"access,omitempty" cborgen:"access,omitempty"`
 	// comment: Comment describing the reason for the override.
 	Comment string `json:"comment" cborgen:"comment"`
 	// status: The status to be set for the user decided by a moderator, overriding whatever value the user had previously. Use reset to default to original state.
@@ -205,6 +211,8 @@ type ModerationDefs_ModEventEmail struct {
 	Comment *string `json:"comment,omitempty" cborgen:"comment,omitempty"`
 	// content: The content of the email sent to the user.
 	Content *string `json:"content,omitempty" cborgen:"content,omitempty"`
+	// isDelivered: Indicates whether the email was successfully delivered to the user's inbox.
+	IsDelivered *bool `json:"isDelivered,omitempty" cborgen:"isDelivered,omitempty"`
 	// policies: Names/Keywords of the policies that necessitated the email.
 	Policies []string `json:"policies,omitempty" cborgen:"policies,omitempty"`
 	// severityLevel: Severity level of the violation. Normally 'sev-1' that adds strike on repeat offense
@@ -330,6 +338,8 @@ type ModerationDefs_ModEventTakedown struct {
 	StrikeCount *int64 `json:"strikeCount,omitempty" cborgen:"strikeCount,omitempty"`
 	// strikeExpiresAt: When the strike should expire. If not provided, the strike never expires.
 	StrikeExpiresAt *string `json:"strikeExpiresAt,omitempty" cborgen:"strikeExpiresAt,omitempty"`
+	// targetServices: List of services where the takedown should be applied. If empty or not provided, takedown is applied on all configured services.
+	TargetServices []string `json:"targetServices,omitempty" cborgen:"targetServices,omitempty"`
 }
 
 // ModerationDefs_ModEventUnmute is a "modEventUnmute" in the tools.ozone.moderation.defs schema.
diff --git a/api/ozone/moderationscheduleAction.go b/api/ozone/moderationscheduleAction.go
index 2edd18d7..728fdf5a 100644
--- a/api/ozone/moderationscheduleAction.go
+++ b/api/ozone/moderationscheduleAction.go
@@ -85,8 +85,18 @@ type ModerationScheduleAction_Takedown struct {
 	Comment                    *string `json:"comment,omitempty" cborgen:"comment,omitempty"`
 	// durationInHours: Indicates how long the takedown should be in effect before automatically expiring.
 	DurationInHours *int64 `json:"durationInHours,omitempty" cborgen:"durationInHours,omitempty"`
+	// emailContent: Email content to be sent to the user upon takedown.
+	EmailContent *string `json:"emailContent,omitempty" cborgen:"emailContent,omitempty"`
+	// emailSubject: Subject of the email to be sent to the user upon takedown.
+	EmailSubject *string `json:"emailSubject,omitempty" cborgen:"emailSubject,omitempty"`
 	// policies: Names/Keywords of the policies that drove the decision.
 	Policies []string `json:"policies,omitempty" cborgen:"policies,omitempty"`
+	// severityLevel: Severity level of the violation (e.g., 'sev-0', 'sev-1', 'sev-2', etc.).
+	SeverityLevel *string `json:"severityLevel,omitempty" cborgen:"severityLevel,omitempty"`
+	// strikeCount: Number of strikes to assign to the user when takedown is applied.
+	StrikeCount *int64 `json:"strikeCount,omitempty" cborgen:"strikeCount,omitempty"`
+	// strikeExpiresAt: When the strike should expire. If not provided, the strike never expires.
+	StrikeExpiresAt *string `json:"strikeExpiresAt,omitempty" cborgen:"strikeExpiresAt,omitempty"`
 }
 
 // ModerationScheduleAction calls the XRPC method "tools.ozone.moderation.scheduleAction".
diff --git a/atproto/atcrypto/keys.go b/atproto/atcrypto/keys.go
index 6f43b655..8e94a43f 100644
--- a/atproto/atcrypto/keys.go
+++ b/atproto/atcrypto/keys.go
@@ -69,7 +69,7 @@ type PublicKey interface {
 	JWK() (*JWK, error)
 }
 
-var ErrInvalidSignature = errors.New("crytographic signature invalid")
+var ErrInvalidSignature = errors.New("cryptographic signature invalid")
 
 /*
 // quick code to verify varint byte conversion (https://play.golang.com/):
diff --git a/atproto/auth/oauth/doc.go b/atproto/auth/oauth/doc.go
index d77985ba..85b70d25 100644
--- a/atproto/auth/oauth/doc.go
+++ b/atproto/auth/oauth/doc.go
@@ -36,7 +36,7 @@ Create a single [ClientApp] instance during service setup that will be used (con
 
 	oauthApp := oauth.NewClientApp(&config, oauth.NewMemStore())
 
-For a real service, you would want to use a database or other peristant implementation of the [ClientAuthStore] interface instead of [MemStore]. Otherwise all user sessions are dropped every time the process restarts.
+For a real service, you would want to use a database or other persistant implementation of the [ClientAuthStore] interface instead of [MemStore]. Otherwise all user sessions are dropped every time the process restarts.
 
 The client metadata document needs to be served at the URL indicated by the 'client_id'. This can be done statically, or dynamically generated and served from the configuration:
 
@@ -140,6 +140,6 @@ In these scenarios, applications could use an implementation of [ClientAuthStore
 
 In the traditional web app backend scenario, a single account (DID) might have multiple active sessions. For example, a user might log in from a browser on their laptop and on a mobile device at the same time. The user must go through the entire flow on each device (or browser) to authenticate the user. To prevent a new session from "clobbering" existing sessions (including tokens), this package supports multiple concurrent sessions per account, distinguished by a session ID. The random 'state' token from the auth flow is re-used by default.
 
-In other scenarious, multiple sessions are not needed or desirable. For example, an integration backend, or tool with very short session lifetimes. In these scenarios, implementations of the [ClientAuthStore] interface could ignore the session ID. Or the [ClientApp] could be configured with an ephemeral [ClientAuthStore] (to support auth flows), and managed the session data returned by [ClientApp.ProcessCallback] using separate session storage logic.
+In other scenarios, multiple sessions are not needed or desirable. For example, an integration backend, or tool with very short session lifetimes. In these scenarios, implementations of the [ClientAuthStore] interface could ignore the session ID. Or the [ClientApp] could be configured with an ephemeral [ClientAuthStore] (to support auth flows), and managed the session data returned by [ClientApp.ProcessCallback] using separate session storage logic.
 */
 package oauth
diff --git a/atproto/identity/identity.go b/atproto/identity/identity.go
index 663c6c0f..3dde38f8 100644
--- a/atproto/identity/identity.go
+++ b/atproto/identity/identity.go
@@ -24,7 +24,7 @@ type Identity struct {
 	Keys        map[string]VerificationMethod
 }
 
-// Sub-field type for [Identity], representing a crytographic public key declared as a "verificationMethod" in the DID document.
+// Sub-field type for [Identity], representing a cryptographic public key declared as a "verificationMethod" in the DID document.
 type VerificationMethod struct {
 	Type               string
 	PublicKeyMultibase string
diff --git a/atproto/lexicon/extract.go b/atproto/lexicon/extract_type.go
similarity index 100%
rename from atproto/lexicon/extract.go
rename to atproto/lexicon/extract_type.go
diff --git a/atproto/lexicon/language.go b/atproto/lexicon/language.go
index 2631daec..64fad98f 100644
--- a/atproto/lexicon/language.go
+++ b/atproto/lexicon/language.go
@@ -32,8 +32,6 @@ func (s *SchemaDef) CheckSchema() error {
 		return v.CheckSchema()
 	case SchemaPermission:
 		return v.CheckSchema()
-	case SchemaNull:
-		return v.CheckSchema()
 	case SchemaBoolean:
 		return v.CheckSchema()
 	case SchemaInteger:
@@ -65,6 +63,44 @@ func (s *SchemaDef) CheckSchema() error {
 	}
 }
 
+// Checks if def is of "parimary" type
+func (s *SchemaDef) IsPrimary() bool {
+	switch s.Inner.(type) {
+	case SchemaRecord, SchemaQuery, SchemaProcedure, SchemaSubscription, SchemaPermissionSet:
+		return true
+	}
+	return false
+}
+
+func (s *SchemaDef) IsConcrete() bool {
+	switch s.Inner.(type) {
+	case SchemaBoolean, SchemaInteger, SchemaString, SchemaBytes, SchemaCIDLink, SchemaBlob:
+		return true
+	}
+	return false
+}
+
+// Checks if type can be a top-level "named" definition
+func (s *SchemaDef) IsDefinable() bool {
+	switch s.Inner.(type) {
+	case SchemaUnknown, SchemaUnion, SchemaRef, SchemaParams, SchemaPermission:
+		return false
+	}
+	return true
+}
+
+// Checks if the type is allowed to be included in object fields.
+func (s *SchemaDef) IsFieldType() bool {
+	if s.IsConcrete() {
+		return true
+	}
+	switch s.Inner.(type) {
+	case SchemaArray, SchemaObject, SchemaRef, SchemaUnion, SchemaUnknown:
+		return true
+	}
+	return false
+}
+
 // Helper to recurse down the definition tree and set full references on any sub-schemas which need to embed that metadata
 func (s *SchemaDef) setBase(base string) {
 	switch v := s.Inner.(type) {
@@ -107,7 +143,10 @@ func (s *SchemaDef) setBase(base string) {
 			}
 		}
 		if v.Message != nil {
-			v.Message.Schema.setBase(base)
+			u := SchemaDef{Inner: v.Message.Schema}
+			u.setBase(base)
+			i := u.Inner.(SchemaUnion)
+			v.Message.Schema = i
 		}
 		s.Inner = v
 	case SchemaArray:
@@ -199,13 +238,6 @@ func (s *SchemaDef) UnmarshalJSON(b []byte) error {
 		}
 		s.Inner = *v
 		return nil
-	case "null":
-		v := new(SchemaNull)
-		if err = json.Unmarshal(b, v); err != nil {
-			return err
-		}
-		s.Inner = *v
-		return nil
 	case "boolean":
 		v := new(SchemaBoolean)
 		if err = json.Unmarshal(b, v); err != nil {
@@ -310,7 +342,12 @@ type SchemaRecord struct {
 }
 
 func (s *SchemaRecord) CheckSchema() error {
+	if s.Type != "record" {
+		return fmt.Errorf("expected 'record' schema")
+	}
 	switch s.Key {
+	case "":
+		return fmt.Errorf("record key specifier is required")
 	case "tid", "nsid", "any":
 		// pass
 	default:
@@ -330,6 +367,9 @@ type SchemaQuery struct {
 }
 
 func (s *SchemaQuery) CheckSchema() error {
+	if s.Type != "query" {
+		return fmt.Errorf("expected 'query' schema")
+	}
 	if s.Output != nil {
 		if err := s.Output.CheckSchema(); err != nil {
 			return err
@@ -358,6 +398,9 @@ type SchemaProcedure struct {
 }
 
 func (s *SchemaProcedure) CheckSchema() error {
+	if s.Type != "procedure" {
+		return fmt.Errorf("expected 'procedure' schema")
+	}
 	if s.Input != nil {
 		if err := s.Input.CheckSchema(); err != nil {
 			return err
@@ -389,6 +432,9 @@ type SchemaSubscription struct {
 }
 
 func (s *SchemaSubscription) CheckSchema() error {
+	if s.Type != "subscription" {
+		return fmt.Errorf("expected 'subscription' schema")
+	}
 	if s.Message != nil {
 		if err := s.Message.CheckSchema(); err != nil {
 			return err
@@ -404,6 +450,7 @@ func (s *SchemaSubscription) CheckSchema() error {
 
 type SchemaPermissionSet struct {
 	Type        string             `json:"type"` // "permission-set"
+	Description *string            `json:"description,omitempty"`
 	Title       *string            `json:"title,omitempty"`
 	TitleLangs  map[string]string  `json:"title:langs,omitempty"`
 	Detail      *string            `json:"detail,omitempty"`
@@ -412,6 +459,9 @@ type SchemaPermissionSet struct {
 }
 
 func (s *SchemaPermissionSet) CheckSchema() error {
+	if s.Type != "permission-set" {
+		return fmt.Errorf("expected 'permission-set' schema")
+	}
 	for lang, _ := range s.TitleLangs {
 		_, err := syntax.ParseLanguage(lang)
 		if err != nil {
@@ -437,7 +487,6 @@ type SchemaPermission struct {
 	Description *string `json:"description,omitempty"`
 
 	Resource   string   `json:"resource"`
-	Accept     []string `json:"accept,omitempty"`
 	Collection []string `json:"collection,omitempty"`
 	Action     []string `json:"action,omitempty"`
 	LXM        []string `json:"lxm,omitempty"`
@@ -447,27 +496,16 @@ type SchemaPermission struct {
 
 func (s *SchemaPermission) CheckSchema() error {
 	if s.Type != "permission" {
-		return fmt.Errorf("expected 'permission'")
+		return fmt.Errorf("expected 'permission' schema")
 	}
 	switch s.Resource {
-	case "blob":
-		if len(s.Accept) == 0 {
-			return fmt.Errorf("blob permission requires 'accept'")
-		}
-		for _, acc := range s.Accept {
-			// TODO: more complete MIME pattern parsing
-			parts := strings.SplitN(acc, "/", 3)
-			if len(parts) != 2 || parts[0] == "*" || parts[0] == "" || parts[1] == "" {
-				return fmt.Errorf("invalid blob 'accept' pattern: %s", acc)
-			}
-		}
 	case "repo":
 		if len(s.Collection) == 0 {
 			return fmt.Errorf("repo permission requires 'collection'")
 		}
 		for _, coll := range s.Collection {
 			if coll == "*" {
-				continue
+				return fmt.Errorf("repo permission does not support wildcard when in permission set")
 			}
 			_, err := syntax.ParseNSID(coll)
 			if err != nil {
@@ -476,7 +514,7 @@ func (s *SchemaPermission) CheckSchema() error {
 		}
 		for _, act := range s.Action {
 			if act != "create" && act != "update" && act != "delete" {
-				return fmt.Errorf("unsupported repo action: %s", act)
+				return fmt.Errorf("repo permission unsupported action: %s", act)
 			}
 		}
 	case "rpc":
@@ -487,7 +525,7 @@ func (s *SchemaPermission) CheckSchema() error {
 			if lxm == "*" {
 				if s.Audience == "*" {
 					// TODO: is this necessary here?
-					return fmt.Errorf("can't have both 'lxm' and 'aud' be '*'")
+					return fmt.Errorf("rpc permission can't have both 'lxm' and 'aud' be '*'")
 				}
 				continue
 			}
@@ -500,16 +538,10 @@ func (s *SchemaPermission) CheckSchema() error {
 			return fmt.Errorf("rpc permission must have eith 'aud' or 'inheritAud' defined")
 		}
 		if s.Audience != "" && s.Audience != "*" {
-			// TODO: helper for service refs
-			parts := strings.SplitN(s.Audience, "#", 3)
-			if len(parts) != 2 || parts[1] == "" {
-				return fmt.Errorf("rpc 'aud' must be a service ref")
-			}
-			_, err := syntax.ParseDID(parts[0])
-			if err != nil {
-				return fmt.Errorf("rpc 'aud' must be a service ref: %w", err)
-			}
+			return fmt.Errorf("rpc permission 'aud' can't have service DID in permission set")
 		}
+	case "blob", "account", "identity":
+		return fmt.Errorf("%s permission not allowed in permission sets", s.Resource)
 	default:
 		return fmt.Errorf("unsupported permission resource: %s", s.Resource)
 	}
@@ -539,14 +571,11 @@ func (s *SchemaBody) CheckSchema() error {
 }
 
 type SchemaMessage struct {
-	Description *string   `json:"description,omitempty"`
-	Schema      SchemaDef `json:"schema"` // required; type:union only
+	Description *string     `json:"description,omitempty"`
+	Schema      SchemaUnion `json:"schema"` // required; type:union only
 }
 
 func (s *SchemaMessage) CheckSchema() error {
-	if _, ok := s.Schema.Inner.(SchemaUnion); !ok {
-		return fmt.Errorf("message must have schema type union")
-	}
 	return s.Schema.CheckSchema()
 }
 
@@ -573,30 +602,17 @@ func (s *SchemaError) Validate(d any) error {
 	return nil
 }
 
-type SchemaNull struct {
-	Type        string  `json:"type"` // "null"
-	Description *string `json:"description,omitempty"`
-}
-
-func (s *SchemaNull) CheckSchema() error {
-	return nil
-}
-
-func (s *SchemaNull) Validate(d any) error {
-	if d != nil {
-		return fmt.Errorf("expected null data, got: %s", reflect.TypeOf(d))
-	}
-	return nil
-}
-
 type SchemaBoolean struct {
-	Type        string  `json:"type"` // "bool"
+	Type        string  `json:"type"` // "boolean"
 	Description *string `json:"description,omitempty"`
 	Default     *bool   `json:"default,omitempty"`
 	Const       *bool   `json:"const,omitempty"`
 }
 
 func (s *SchemaBoolean) CheckSchema() error {
+	if s.Type != "boolean" {
+		return fmt.Errorf("expected 'boolean' schema")
+	}
 	if s.Default != nil && s.Const != nil {
 		return fmt.Errorf("schema can't have both 'default' and 'const'")
 	}
@@ -625,6 +641,9 @@ type SchemaInteger struct {
 }
 
 func (s *SchemaInteger) CheckSchema() error {
+	if s.Type != "integer" {
+		return fmt.Errorf("expected 'integer' schema")
+	}
 	// TODO: enforce min/max against enum, default, const
 	if s.Default != nil && s.Const != nil {
 		return fmt.Errorf("schema can't have both 'default' and 'const'")
@@ -677,6 +696,9 @@ type SchemaString struct {
 }
 
 func (s *SchemaString) CheckSchema() error {
+	if s.Type != "string" {
+		return fmt.Errorf("expected 'string' schema")
+	}
 	// TODO: enforce min/max against enum, default, const
 	if s.Default != nil && s.Const != nil {
 		return fmt.Errorf("schema can't have both 'default' and 'const'")
@@ -799,6 +821,9 @@ type SchemaBytes struct {
 }
 
 func (s *SchemaBytes) CheckSchema() error {
+	if s.Type != "bytes" {
+		return fmt.Errorf("expected 'bytes' schema")
+	}
 	if s.MinLength != nil && s.MaxLength != nil && *s.MaxLength < *s.MinLength {
 		return fmt.Errorf("schema max < min")
 	}
@@ -826,6 +851,9 @@ type SchemaCIDLink struct {
 }
 
 func (s *SchemaCIDLink) CheckSchema() error {
+	if s.Type != "cid-link" {
+		return fmt.Errorf("expected 'cid-link' schema")
+	}
 	return nil
 }
 
@@ -846,6 +874,9 @@ type SchemaArray struct {
 }
 
 func (s *SchemaArray) CheckSchema() error {
+	if s.Type != "array" {
+		return fmt.Errorf("expected 'array' schema")
+	}
 	if s.MinLength != nil && s.MaxLength != nil && *s.MaxLength < *s.MinLength {
 		return fmt.Errorf("schema max < min")
 	}
@@ -853,6 +884,9 @@ func (s *SchemaArray) CheckSchema() error {
 		(s.MaxLength != nil && *s.MaxLength < 0) {
 		return fmt.Errorf("array schema min or max below zero")
 	}
+	if !s.Items.IsFieldType() {
+		return fmt.Errorf("array schema elements not allowed type")
+	}
 	return s.Items.CheckSchema()
 }
 
@@ -865,6 +899,9 @@ type SchemaObject struct {
 }
 
 func (s *SchemaObject) CheckSchema() error {
+	if s.Type != "object" {
+		return fmt.Errorf("expected 'object' schema")
+	}
 	// TODO: check for set intersection between required and nullable
 	// TODO: check for set uniqueness of required and nullable
 	for _, k := range s.Required {
@@ -882,6 +919,9 @@ func (s *SchemaObject) CheckSchema() error {
 		if len(k) == 0 {
 			return fmt.Errorf("empty object schema field name not allowed")
 		}
+		if !def.IsFieldType() {
+			return fmt.Errorf("object schema property not an allowed type")
+		}
 		if err := def.CheckSchema(); err != nil {
 			return err
 		}
@@ -907,6 +947,9 @@ type SchemaBlob struct {
 }
 
 func (s *SchemaBlob) CheckSchema() error {
+	if s.Type != "blob" {
+		return fmt.Errorf("expected 'blob' schema")
+	}
 	// TODO: validate Accept (mimetypes)?
 	if s.MaxSize != nil && *s.MaxSize <= 0 {
 		return fmt.Errorf("blob max size less or equal to zero")
@@ -949,7 +992,7 @@ type SchemaParams struct {
 
 func (s *SchemaParams) CheckSchema() error {
 	if s.Type != "params" {
-		return fmt.Errorf("expected 'params'")
+		return fmt.Errorf("expected 'params' schema")
 	}
 	// TODO: check for set uniqueness of required
 	for _, k := range s.Required {
@@ -963,17 +1006,17 @@ func (s *SchemaParams) CheckSchema() error {
 			return fmt.Errorf("empty object schema field name not allowed")
 		}
 		switch v := def.Inner.(type) {
-		case SchemaBoolean, SchemaInteger, SchemaString, SchemaUnknown:
+		case SchemaBoolean, SchemaInteger, SchemaString:
 			// pass
 		case SchemaArray:
 			switch v.Items.Inner.(type) {
-			case SchemaBoolean, SchemaInteger, SchemaString, SchemaUnknown:
+			case SchemaBoolean, SchemaInteger, SchemaString:
 				// pass
 			default:
-				return fmt.Errorf("params array item type must be boolean, integer, string, or unknown")
+				return fmt.Errorf("params array item type must be boolean, integer, or string")
 			}
 		default:
-			return fmt.Errorf("params field type must be boolean, integer, string, or unknown")
+			return fmt.Errorf("params field type must be boolean, integer, string, or array")
 		}
 		if err := def.CheckSchema(); err != nil {
 			return err
@@ -990,6 +1033,9 @@ type SchemaToken struct {
 }
 
 func (s *SchemaToken) CheckSchema() error {
+	if s.Type != "token" {
+		return fmt.Errorf("expected 'token' schema")
+	}
 	if s.FullName == "" {
 		return fmt.Errorf("expected fully-qualified token name")
 	}
@@ -997,17 +1043,7 @@ func (s *SchemaToken) CheckSchema() error {
 }
 
 func (s *SchemaToken) Validate(d any) error {
-	str, ok := d.(string)
-	if !ok {
-		return fmt.Errorf("expected a string for token, got: %s", reflect.TypeOf(d))
-	}
-	if s.FullName == "" {
-		return fmt.Errorf("token name was not populated at parse time")
-	}
-	if str != s.FullName {
-		return fmt.Errorf("token name did not match expected: %s", str)
-	}
-	return nil
+	return fmt.Errorf("token type does not validate against data")
 }
 
 type SchemaRef struct {
@@ -1019,6 +1055,9 @@ type SchemaRef struct {
 }
 
 func (s *SchemaRef) CheckSchema() error {
+	if s.Type != "ref" {
+		return fmt.Errorf("expected 'ref' schema")
+	}
 	// TODO: more validation of ref string?
 	if len(s.Ref) == 0 {
 		return fmt.Errorf("empty schema ref")
@@ -1039,6 +1078,12 @@ type SchemaUnion struct {
 }
 
 func (s *SchemaUnion) CheckSchema() error {
+	if s.Type != "union" {
+		return fmt.Errorf("expected 'union' schema")
+	}
+	if len(s.Refs) == 0 && s.Closed != nil && *s.Closed {
+		return fmt.Errorf("closed empty unions are not allowed")
+	}
 	// TODO: uniqueness check on refs
 	for _, ref := range s.Refs {
 		// TODO: more validation of ref string?
@@ -1058,6 +1103,9 @@ type SchemaUnknown struct {
 }
 
 func (s *SchemaUnknown) CheckSchema() error {
+	if s.Type != "unknown" {
+		return fmt.Errorf("expected 'unknown' schema")
+	}
 	return nil
 }
 
diff --git a/atproto/lexicon/mimetype.go b/atproto/lexicon/mimetype.go
index 0e42d5ed..5b929962 100644
--- a/atproto/lexicon/mimetype.go
+++ b/atproto/lexicon/mimetype.go
@@ -9,6 +9,9 @@ func acceptableMimeType(pattern, val string) bool {
 	if val == "" || pattern == "" {
 		return false
 	}
+	if pattern == "*/*" {
+		return true
+	}
 	if strings.HasSuffix(pattern, "*") {
 		prefix := pattern[:len(pattern)-1]
 		return strings.HasPrefix(val, prefix)
diff --git a/atproto/lexicon/resolve.go b/atproto/lexicon/resolve.go
index ed018a74..3bd5db07 100644
--- a/atproto/lexicon/resolve.go
+++ b/atproto/lexicon/resolve.go
@@ -7,10 +7,10 @@ import (
 	"log/slog"
 
 	"github.com/bluesky-social/indigo/api/agnostic"
+	"github.com/bluesky-social/indigo/atproto/atclient"
 	"github.com/bluesky-social/indigo/atproto/atdata"
 	"github.com/bluesky-social/indigo/atproto/identity"
 	"github.com/bluesky-social/indigo/atproto/syntax"
-	"github.com/bluesky-social/indigo/xrpc"
 )
 
 // Low-level routine for resolving an NSID to full Lexicon data record (as stored in a repository).
@@ -73,10 +73,8 @@ func resolveLexiconJSON(ctx context.Context, dir identity.Directory, nsid syntax
 func fetchRecordJSON(ctx context.Context, ident identity.Identity, aturi syntax.ATURI) (*json.RawMessage, error) {
 
 	slog.Debug("fetching record", "did", ident.DID.String(), "collection", aturi.Collection().String(), "rkey", aturi.RecordKey().String())
-	xrpcc := xrpc.Client{
-		Host: ident.PDSEndpoint(),
-	}
-	resp, err := agnostic.RepoGetRecord(ctx, &xrpcc, "", aturi.Collection().String(), ident.DID.String(), aturi.RecordKey().String())
+	client := atclient.NewAPIClient(ident.PDSEndpoint())
+	resp, err := agnostic.RepoGetRecord(ctx, client, "", aturi.Collection().String(), ident.DID.String(), aturi.RecordKey().String())
 	if err != nil {
 		return nil, err
 	}
diff --git a/atproto/lexicon/schemafile.go b/atproto/lexicon/schemafile.go
index 3a0a8b77..ea60e7d7 100644
--- a/atproto/lexicon/schemafile.go
+++ b/atproto/lexicon/schemafile.go
@@ -52,14 +52,13 @@ func (sf *SchemaFile) CheckSchema() error {
 	for frag, def := range sf.Defs {
 		if len(frag) == 0 || strings.Contains(frag, "#") || strings.Contains(frag, ".") {
 			// TODO: more validation here?
-			return fmt.Errorf("schema name invalid: %s", frag)
+			return fmt.Errorf("schema def name invalid: %s", frag)
+		}
+		if def.IsPrimary() && frag != "main" {
+			return fmt.Errorf("'primary' definition types can only have name 'main', not: %s", frag)
 		}
-		// "A file can have at most one definition with one of the "primary" types. Primary types should always have the name main. It is possible for main to describe a non-primary type."
-		switch def.Inner.(type) {
-		case SchemaRecord, SchemaQuery, SchemaProcedure, SchemaSubscription, SchemaPermissionSet:
-			if frag != "main" {
-				return fmt.Errorf("record, query, procedure, and subscription types must be 'main', not: %s", frag)
-			}
+		if !def.IsDefinable() {
+			return fmt.Errorf("schema type can not be used as a named definition: %s", frag)
 		}
 		if err := def.CheckSchema(); err != nil {
 			return err
diff --git a/atproto/lexicon/lexicon.go b/atproto/lexicon/validation.go
similarity index 99%
rename from atproto/lexicon/lexicon.go
rename to atproto/lexicon/validation.go
index acefb90d..c227b06d 100644
--- a/atproto/lexicon/lexicon.go
+++ b/atproto/lexicon/validation.go
@@ -57,8 +57,6 @@ func validateRecordConfig(cat Catalog, recordData any, ref string, flags Validat
 
 func validateData(cat Catalog, def any, d any, flags ValidateFlags) error {
 	switch v := def.(type) {
-	case SchemaNull:
-		return v.Validate(d)
 	case SchemaBoolean:
 		return v.Validate(d)
 	case SchemaInteger:
diff --git a/cmd/beemo/Dockerfile b/cmd/beemo/Dockerfile
index c34b3833..49f56825 100644
--- a/cmd/beemo/Dockerfile
+++ b/cmd/beemo/Dockerfile
@@ -3,7 +3,7 @@
 #   podman build -f ./cmd/beemo/Dockerfile -t beemo .
 
 ### Compile stage
-FROM golang:1.24-alpine3.22 AS build-env
+FROM golang:1.25-alpine3.22 AS build-env
 RUN apk add --no-cache build-base make git
 
 ADD . /dockerbuild
diff --git a/cmd/bigsky/Dockerfile b/cmd/bigsky/Dockerfile
index 96dbc377..35a93c44 100644
--- a/cmd/bigsky/Dockerfile
+++ b/cmd/bigsky/Dockerfile
@@ -3,7 +3,7 @@
 #   podman build -f ./cmd/bigsky/Dockerfile -t bigsky .
 
 ### Compile stage
-FROM golang:1.24-alpine3.22 AS build-env
+FROM golang:1.25-alpine3.22 AS build-env
 RUN apk add --no-cache build-base make git
 
 ADD . /dockerbuild
diff --git a/cmd/bluepages/Dockerfile b/cmd/bluepages/Dockerfile
index 4d3bec2c..d36149ed 100644
--- a/cmd/bluepages/Dockerfile
+++ b/cmd/bluepages/Dockerfile
@@ -3,7 +3,7 @@
 #   podman build -f ./cmd/bluepages/Dockerfile -t bluepages .
 
 ### Compile stage
-FROM golang:1.24-alpine3.22 AS build-env
+FROM golang:1.25-alpine3.22 AS build-env
 RUN apk add --no-cache build-base make git
 
 ADD . /dockerbuild
diff --git a/cmd/collectiondir/Dockerfile b/cmd/collectiondir/Dockerfile
index 7757baea..6c9819ed 100644
--- a/cmd/collectiondir/Dockerfile
+++ b/cmd/collectiondir/Dockerfile
@@ -1,4 +1,4 @@
-FROM golang:1.24-bullseye AS build-env
+FROM golang:1.25-bookworm AS build-env
 
 ENV DEBIAN_FRONTEND=noninteractive
 ENV TZ=Etc/UTC
@@ -21,7 +21,7 @@ RUN go build \
     -o /collectiondir-bin \
     ./cmd/collectiondir
 
-FROM debian:bullseye-slim
+FROM debian:bookworm-slim
 
 ENV DEBIAN_FRONTEND="noninteractive"
 ENV TZ=Etc/UTC
diff --git a/cmd/hepa/Dockerfile b/cmd/hepa/Dockerfile
index 72e3a306..0bce5cfb 100644
--- a/cmd/hepa/Dockerfile
+++ b/cmd/hepa/Dockerfile
@@ -3,7 +3,7 @@
 #   podman build -f ./cmd/hepa/Dockerfile -t hepa .
 
 ### Compile stage
-FROM golang:1.24-alpine3.22 AS build-env
+FROM golang:1.25-alpine3.22 AS build-env
 RUN apk add --no-cache build-base make git
 
 ADD . /dockerbuild
diff --git a/cmd/palomar/Dockerfile b/cmd/palomar/Dockerfile
index 0a235cc0..3d003970 100644
--- a/cmd/palomar/Dockerfile
+++ b/cmd/palomar/Dockerfile
@@ -3,7 +3,7 @@
 #   podman build -f ./cmd/palomar/Dockerfile -t palomar .
 
 ### Compile stage
-FROM golang:1.24-alpine3.22 AS build-env
+FROM golang:1.25-alpine3.22 AS build-env
 RUN apk add --no-cache build-base make git
 
 ADD . /dockerbuild
diff --git a/cmd/rainbow/Dockerfile b/cmd/rainbow/Dockerfile
index c36f4595..b6270aa4 100644
--- a/cmd/rainbow/Dockerfile
+++ b/cmd/rainbow/Dockerfile
@@ -1,4 +1,4 @@
-FROM golang:1.24-bullseye AS build-env
+FROM golang:1.25-bookworm AS build-env
 
 ENV DEBIAN_FRONTEND=noninteractive
 ENV TZ=Etc/UTC
@@ -21,7 +21,7 @@ RUN go build \
     -o /rainbow-bin \
     ./cmd/rainbow
 
-FROM debian:bullseye-slim
+FROM debian:bookworm-slim
 
 ENV DEBIAN_FRONTEND="noninteractive"
 ENV TZ=Etc/UTC
diff --git a/cmd/relay/Dockerfile b/cmd/relay/Dockerfile
index 9eee1a05..926b728f 100644
--- a/cmd/relay/Dockerfile
+++ b/cmd/relay/Dockerfile
@@ -3,7 +3,7 @@
 #   podman build -f ./cmd/relay/Dockerfile -t relay .
 
 ### Compile stage
-FROM golang:1.24-alpine3.22 AS build-env
+FROM golang:1.25-alpine3.22 AS build-env
 RUN apk add --no-cache build-base make git
 
 ADD . /dockerbuild
diff --git a/cmd/relay/handlers.go b/cmd/relay/handlers.go
index d97dfcb9..6ae91770 100644
--- a/cmd/relay/handlers.go
+++ b/cmd/relay/handlers.go
@@ -130,7 +130,7 @@ func (s *Service) handleComAtprotoSyncGetHostStatus(c echo.Context, hostname str
 func (s *Service) handleComAtprotoSyncListRepos(c echo.Context, cursor int64, limit int) (*comatproto.SyncListRepos_Output, error) {
 	ctx := c.Request().Context()
 
-	accounts, err := s.relay.ListAccounts(ctx, cursor, limit)
+	accounts, err := s.relay.ListAccountsDetailed(ctx, cursor, limit)
 	if err != nil {
 		s.logger.Error("failed to query accounts", "err", err)
 		return nil, c.JSON(http.StatusInternalServerError, xrpc.XRPCError{ErrStr: "DatabaseError", Message: "failed to list accounts (repos)"})
@@ -147,30 +147,12 @@ func (s *Service) handleComAtprotoSyncListRepos(c echo.Context, cursor int64, li
 		Repos: make([]*comatproto.SyncListRepos_Repo, len(accounts)),
 	}
 
-	// Fetch the repo roots for each user
-	// TODO: would be much more efficient to do a join and have Relay.ListAccounts return these repos with the account info
 	for i, acc := range accounts {
-		repo, err := s.relay.GetAccountRepo(ctx, acc.UID)
-		if err != nil && !errors.Is(err, relay.ErrAccountRepoNotFound) {
-			s.logger.Error("failed to get repo root", "err", err, "did", acc.DID)
-			return nil, echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to get repo root for (%s): %v", acc.DID, err.Error()))
-		}
-		// note: repo can be nil beyond this point!
-
-		// TODO: empty strings here may not be spec-compliant
-		// need to determine correct handling when there is no account_repo entry
-		// see https://github.com/bluesky-social/indigo/issues/1143
-		var cid, rev string
-		if repo != nil {
-			cid = repo.CommitCID
-			rev = repo.Rev
-		}
-
 		active := acc.IsActive()
 		resp.Repos[i] = &comatproto.SyncListRepos_Repo{
 			Did:    acc.DID,
-			Head:   cid,
-			Rev:    rev,
+			Head:   acc.CommitCID,
+			Rev:    acc.Rev,
 			Active: &active,
 			Status: acc.StatusField(),
 		}
diff --git a/cmd/relay/relay/account.go b/cmd/relay/relay/account.go
index fad029f7..46306dde 100644
--- a/cmd/relay/relay/account.go
+++ b/cmd/relay/relay/account.go
@@ -237,13 +237,27 @@ func (r *Relay) UpdateAccountLocalStatus(ctx context.Context, did syntax.DID, st
 
 // Returns the of active accounts (based on local and upstream status). The sort order is by UID, ascending.
 func (r *Relay) ListAccounts(ctx context.Context, cursor int64, limit int) ([]*models.Account, error) {
-
 	accounts := []*models.Account{}
 	if err := r.db.WithContext(ctx).Model(&models.Account{}).Where("uid > ? AND status = 'active' AND upstream_status = 'active'", cursor).Order("uid").Limit(limit).Find(&accounts).Error; err != nil {
 		return nil, err
 	}
 	return accounts, nil
 }
+func (r *Relay) ListAccountsDetailed(ctx context.Context, cursor int64, limit int) ([]*models.AccountDetailed, error) {
+	accounts := []*models.AccountDetailed{}
+	if err := r.db.WithContext(ctx).Raw(`
+		SELECT account.uid, account.did, account.host_id, account.status, account.upstream_status,
+		       account_repo.rev, account_repo.commit_cid, account_repo.commit_data_cid
+		FROM account
+		INNER JOIN account_repo ON account.uid = account_repo.uid
+		WHERE account.uid > ? AND account.status = 'active' AND account.upstream_status = 'active'
+		ORDER BY account.uid
+		LIMIT ?
+	`, cursor, limit).Scan(&accounts).Error; err != nil {
+		return nil, err
+	}
+	return accounts, nil
+}
 
 func (r *Relay) ListAccountTakedowns(ctx context.Context, cursor int64, limit int) ([]*models.Account, error) {
 
diff --git a/cmd/relay/relay/ingest.go b/cmd/relay/relay/ingest.go
index e507cba7..fc350c0e 100644
--- a/cmd/relay/relay/ingest.go
+++ b/cmd/relay/relay/ingest.go
@@ -99,7 +99,8 @@ func (r *Relay) processCommitEvent(ctx context.Context, evt *comatproto.SyncSubs
 	}
 
 	if !acc.IsActive() {
-		logger.Info("dropping commit message for non-active account", "status", acc.Status, "upstreamStatus", acc.UpstreamStatus)
+		logger.Info("dropping commit message for inactive-active account", "status", acc.Status, "upstreamStatus", acc.UpstreamStatus)
+		eventsWarningsCounter.WithLabelValues(hostname, "inactive-account").Add(1)
 		return nil
 	}
 
@@ -158,7 +159,8 @@ func (r *Relay) processSyncEvent(ctx context.Context, evt *comatproto.SyncSubscr
 	}
 
 	if !acc.IsActive() {
-		logger.Info("dropping sync message for non-active account", "status", acc.Status, "upstreamStatus", acc.UpstreamStatus)
+		logger.Info("dropping sync message for inactive-active account", "status", acc.Status, "upstreamStatus", acc.UpstreamStatus)
+		eventsWarningsCounter.WithLabelValues(hostname, "inactive-account").Add(1)
 		return nil
 	}
 
diff --git a/cmd/relay/relay/models/joins.go b/cmd/relay/relay/models/joins.go
new file mode 100644
index 00000000..f16829d4
--- /dev/null
+++ b/cmd/relay/relay/models/joins.go
@@ -0,0 +1,37 @@
+package models
+
+type AccountDetailed struct {
+	UID            uint64        `gorm:"column:uid"`
+	DID            string        `gorm:"column:did"`
+	HostID         uint64        `gorm:"column:host_id"`
+	Status         AccountStatus `gorm:"column:status"`
+	UpstreamStatus AccountStatus `gorm:"column:upstream_status"`
+	Rev            string        `gorm:"column:rev"`
+	CommitCID      string        `gorm:"column:commit_cid"`
+	CommitDataCID  string        `gorm:"column:commit_data_cid"`
+}
+
+func (a *AccountDetailed) AccountStatus() AccountStatus {
+	if a.Status != AccountStatusActive {
+		if a.Status == AccountStatusHostThrottled {
+			return AccountStatusThrottled
+		}
+		return a.Status
+	}
+	return a.UpstreamStatus
+}
+
+// Returns a pointer to a copy of status string; or nil if status is active.
+//
+// Helpful for account info responses which have a boolean 'active' and optional 'status' field (like the #account message)
+func (a *AccountDetailed) StatusField() *string {
+	if a.IsActive() {
+		return nil
+	}
+	s := string(a.AccountStatus())
+	return &s
+}
+
+func (a *AccountDetailed) IsActive() bool {
+	return (a.Status == AccountStatusActive || a.Status == AccountStatusThrottled) && a.UpstreamStatus == AccountStatusActive
+}
diff --git a/cmd/relay/relay/slurper.go b/cmd/relay/relay/slurper.go
index bc87f9ea..67bc2cb5 100644
--- a/cmd/relay/relay/slurper.go
+++ b/cmd/relay/relay/slurper.go
@@ -504,7 +504,7 @@ func (s *Slurper) persistCursors(ctx context.Context) error {
 	return err
 }
 
-// gets a snapshot of current subsription hostnames
+// gets a snapshot of current subscription hostnames
 func (s *Slurper) GetActiveSubHostnames() []string {
 	s.subsLk.Lock()
 	defer s.subsLk.Unlock()
diff --git a/cmd/relay/stream/persist/diskpersist/diskpersist.go b/cmd/relay/stream/persist/diskpersist/diskpersist.go
index 516c984d..dba79981 100644
--- a/cmd/relay/stream/persist/diskpersist/diskpersist.go
+++ b/cmd/relay/stream/persist/diskpersist/diskpersist.go
@@ -200,9 +200,21 @@ func (dp *DiskPersistence) resumeLog() error {
 		return fmt.Errorf("failed to scan log file for last seqno: %w", err)
 	}
 
+	if seq == -1 {
+		// The log file is empty, use its starting sequence
+		seq = lfr.SeqStart
+		if seq == 0 {
+			// First file, and no records in it: use the initial sequence number instead
+			seq = dp.initialSeq
+		}
+	} else {
+		// Restart at the last item plus one
+		seq++
+	}
+
 	dp.log.Info("loaded seq", "seq", seq, "now", time.Now().UnixMicro())
 
-	dp.curSeq = seq + 1
+	dp.curSeq = seq
 	dp.logfi = fi
 
 	return nil
@@ -362,7 +374,9 @@ func (dp *DiskPersistence) flushLog(ctx context.Context) error {
 	dp.outbuf.Truncate(0)
 
 	for _, ej := range dp.evtbuf {
-		dp.broadcast(ej.Evt)
+		if dp.broadcast != nil {
+			dp.broadcast(ej.Evt)
+		}
 		ej.Buffer.Truncate(0)
 		dp.buffers.Put(ej.Buffer)
 	}
diff --git a/cmd/sonar/Dockerfile b/cmd/sonar/Dockerfile
index 796b7e2e..8b83ff35 100644
--- a/cmd/sonar/Dockerfile
+++ b/cmd/sonar/Dockerfile
@@ -1,5 +1,5 @@
 # Stage 1: Build the Go binary
-FROM golang:1.24-alpine3.22 AS build-env
+FROM golang:1.25-alpine3.22 AS build-env
 
 # Install SSL ca certificates.
 RUN apk update && apk add --no-cache ca-certificates && update-ca-certificates
diff --git a/cmd/supercollider/Dockerfile b/cmd/supercollider/Dockerfile
index 78bdde81..deda6c6a 100644
--- a/cmd/supercollider/Dockerfile
+++ b/cmd/supercollider/Dockerfile
@@ -1,5 +1,5 @@
 # Stage 1: Build the Go binary
-FROM golang:1.24-alpine3.22 AS build-env
+FROM golang:1.25-alpine3.22 AS build-env
 
 # Create a directory for the application
 WORKDIR /app
diff --git a/go.mod b/go.mod
index 2b90b8d4..b1245380 100644
--- a/go.mod
+++ b/go.mod
@@ -1,8 +1,8 @@
 module github.com/bluesky-social/indigo
 
-go 1.24
+go 1.25
 
-toolchain go1.24.1
+toolchain go1.25.1
 
 require (
 	github.com/PuerkitoBio/purell v1.2.1
diff --git a/lex/lexgen/codegen.go b/lex/lexgen/codegen.go
new file mode 100644
index 00000000..999778f0
--- /dev/null
+++ b/lex/lexgen/codegen.go
@@ -0,0 +1,864 @@
+package lexgen
+
+import (
+	"fmt"
+	"io"
+	"log/slog"
+	"sort"
+	"strings"
+
+	"github.com/bluesky-social/indigo/atproto/lexicon"
+	"github.com/bluesky-social/indigo/atproto/syntax"
+)
+
+// Configuration for [CodeGenerator] output
+type GenConfig struct {
+	RegisterLexiconTypeID bool
+	PackageMappings       map[string]string
+	// one of: "type-decoder", "map-string-any", "json-raw-message"
+	UnknownType string
+	WarningText string
+	LegacyMode  bool
+}
+
+func NewGenConfig() *GenConfig {
+	return &GenConfig{
+		UnknownType: "map-string-any",
+		WarningText: "Code generated by indigo lexgen tool. DO NOT EDIT MANUALLY.",
+	}
+}
+
+func LegacyConfig() *GenConfig {
+	return &GenConfig{
+		RegisterLexiconTypeID: true,
+		UnknownType:           "type-decoder",
+		WarningText:           "Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT.",
+		LegacyMode:            true,
+	}
+}
+
+// Core implementation of Go code generation for a single Lexicon schema file (multiple definitions), building on pre-parsed [FlatLexicon]
+type CodeGenerator struct {
+	Config *GenConfig
+	Lex    *FlatLexicon
+	Cat    lexicon.Catalog
+	Out    io.Writer
+}
+
+// Outputs Go source code to the "Out" [io.Writer].
+func (gen *CodeGenerator) WriteLexicon() error {
+
+	if gen.Config.WarningText != "" {
+		fmt.Fprintf(gen.Out, "// %s\n\n", gen.Config.WarningText)
+	}
+	fmt.Fprintf(gen.Out, "// Lexicon schema: %s\n\n", gen.Lex.NSID)
+	fmt.Fprintf(gen.Out, "package %s\n\n", gen.PkgName())
+	fmt.Fprintln(gen.Out, "import (")
+	for dep, _ := range gen.deps() {
+		fmt.Fprintf(gen.Out, "    %s\n", dep)
+	}
+	fmt.Fprint(gen.Out, ")\n\n")
+
+	for _, ft := range gen.Lex.Types {
+		slog.Debug("generating type", "nsid", gen.Lex.NSID, "def", ft.DefName, "path", ft.Path, "type", ft.Type)
+		if err := gen.WriteType(&ft); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func (gen *CodeGenerator) PkgName() string {
+	n := nsidPkgName(gen.Lex.NSID)
+	if gen.Config.LegacyMode {
+		switch n {
+		case "appbsky":
+			return "bsky"
+		case "comatproto":
+			return "atproto"
+		case "toolsozone":
+			return "ozone"
+		case "chatbsky":
+			return "chat"
+		}
+	}
+	return n
+}
+
+func (gen *CodeGenerator) baseName() string {
+	// TODO: memoize this value? this method gets called a lot
+	return nsidBaseName(gen.Lex.NSID)
+}
+
+func (gen *CodeGenerator) FileName() string {
+	return nsidFileName(gen.Lex.NSID) + ".go"
+}
+
+func (gen *CodeGenerator) deps() map[string]bool {
+	d := map[string]bool{
+		"\"context\"":       true,
+		"\"fmt\"":           true,
+		"\"io\"":            true,
+		"\"bytes\"":         true,
+		"\"encoding/json\"": true,
+		"lexutil \"github.com/bluesky-social/indigo/lex/util\"": true,
+		"cbg \"github.com/whyrusleeping/cbor-gen\"":             true,
+	}
+
+	for ext, _ := range gen.Lex.ExternalRefs {
+		// TODO: replace this with configurable/extensible mappings
+		if strings.HasPrefix(ext, "com.atproto.") {
+			d["comatproto \"github.com/bluesky-social/indigo/api/atproto\""] = true
+		} else if strings.HasPrefix(ext, "app.bsky.") {
+			d["appbsky \"github.com/bluesky-social/indigo/api/bsky\""] = true
+		} else if strings.HasPrefix(ext, "tools.ozone.") {
+			d["toolsozone \"github.com/bluesky-social/indigo/api/ozone\""] = true
+		} else if strings.HasPrefix(ext, "chat.bsky.") {
+			d["chatbsky \"github.com/bluesky-social/indigo/api/chat\""] = true
+		} else {
+			// TODO: configurable mappings; and return error if none found
+			slog.Error("unhandled external reference", "ref", ext)
+		}
+	}
+	return d
+}
+
+func (gen *CodeGenerator) WriteType(ft *FlatType) error {
+
+	switch v := ft.Schema.Inner.(type) {
+	case lexicon.SchemaRecord:
+		if gen.Config.RegisterLexiconTypeID {
+			fmt.Fprintf(gen.Out, "func init() {\n")
+			fmt.Fprintf(gen.Out, "    lexutil.RegisterType(\"%s\", &%s{})", gen.Lex.NSID, gen.baseName())
+			fmt.Fprintf(gen.Out, "}\n\n")
+		}
+		// HACK: insert record-level description in to object if nil
+		if v.Description != nil && v.Record.Description == nil {
+			v.Record.Description = v.Description
+		}
+		if err := gen.writeStruct(ft, &v.Record); err != nil {
+			return err
+		}
+	case lexicon.SchemaQuery:
+		return gen.writeEndpoint(ft, defDescription(ft.Schema), v.Parameters, v.Output, nil, false)
+	case lexicon.SchemaProcedure:
+		return gen.writeEndpoint(ft, defDescription(ft.Schema), v.Parameters, v.Output, v.Input, true)
+	case lexicon.SchemaSubscription:
+		// pass; we only generate message types, not overall subscription
+	case lexicon.SchemaPermissionSet, lexicon.SchemaPermission:
+		// pass for Go codegen
+	case lexicon.SchemaToken:
+		// TODO: pass for now; could be a var/const?
+	case lexicon.SchemaString, lexicon.SchemaInteger, lexicon.SchemaBoolean, lexicon.SchemaUnknown:
+		// skip
+	case lexicon.SchemaObject:
+		if gen.Config.RegisterLexiconTypeID && ft.DefName == "main" && len(ft.Path) == 0 {
+			fmt.Fprintf(gen.Out, "func init() {\n")
+			fmt.Fprintf(gen.Out, "    lexutil.RegisterType(\"%s#main\", &%s{})", gen.Lex.NSID, gen.baseName())
+			fmt.Fprintf(gen.Out, "}\n\n")
+		}
+		if err := gen.writeStruct(ft, &v); err != nil {
+			return err
+		}
+	case lexicon.SchemaUnion:
+		return gen.writeUnion(ft, &v)
+	case lexicon.SchemaRef:
+		// skip for now. could be an alias type?
+	default:
+		return fmt.Errorf("unhandled schema type for codegen: %T", ft.Schema.Inner)
+	}
+
+	return nil
+}
+
+func isRequired(required []string, fname string) bool {
+	for _, k := range required {
+		if k == fname {
+			return true
+		}
+	}
+	return false
+}
+
+func (gen *CodeGenerator) fieldType(fname string, def *lexicon.SchemaDef, optional bool) (string, error) {
+	// NOTE: SchemaObject and SchemaUnion should be handled outside this function; as well as arrays of those types also count
+	// TODO: another pass to check for type completeness
+	switch v := def.Inner.(type) {
+	case lexicon.SchemaBoolean:
+		if optional {
+			return "*bool", nil
+		} else {
+			return "bool", nil
+		}
+	case lexicon.SchemaInteger:
+		if optional {
+			return "*int64", nil
+		} else {
+			return "int64", nil
+		}
+	case lexicon.SchemaString:
+		if optional {
+			return "*string", nil
+		} else {
+			return "string", nil
+		}
+	case lexicon.SchemaBytes:
+		// NOTE: not using a pointer for optional
+		return "lexutil.LexBytes", nil
+	case lexicon.SchemaCIDLink:
+		if optional {
+			return "*lexutil.LexLink", nil
+		} else {
+			return "lexutil.LexLink", nil
+		}
+	case lexicon.SchemaBlob:
+		if optional || gen.Config.LegacyMode {
+			return "*lexutil.LexBlob", nil
+		} else {
+			return "lexutil.LexBlob", nil
+		}
+	case lexicon.SchemaArray:
+		t, err := gen.fieldType(fname, &v.Items, false)
+		if err != nil {
+			return "", err
+		}
+		// NOTE: not using a pointer for optional
+		return "[]" + t, nil
+	case lexicon.SchemaUnknown:
+		switch gen.Config.UnknownType {
+		case "type-decoder":
+			if gen.Config.LegacyMode && (fname == "didDoc" || fname == "plcOp" || fname == "meta" || fname == "debug") {
+				if optional {
+					return "*interface{}", nil
+				} else {
+					return "interface{}", nil
+				}
+			}
+			return "*lexutil.LexiconTypeDecoder", nil
+		case "json-raw-message":
+			if optional {
+				return "*json.RawMessage", nil
+			} else {
+				return "json.RawMessage", nil
+			}
+		case "map-string-any":
+			return "map[string]any", nil
+		default:
+			return "map[string]any", nil
+		}
+	case lexicon.SchemaRef:
+		ptr := ""
+		if optional {
+			ptr = "*"
+		}
+
+		// check for local references to concrete types first
+		if strings.HasPrefix(v.Ref, "#") {
+			dt, ok := gen.Lex.Defs[v.Ref[1:]]
+			if !ok {
+				return "", fmt.Errorf("broken self-reference: %s", v.Ref)
+			}
+			switch dt.Type {
+			case "string":
+				if gen.Config.LegacyMode {
+					ptr = "*"
+				}
+				return ptr + "string", nil
+			case "integer":
+				return ptr + "int64", nil
+			case "boolean":
+				return ptr + "bool", nil
+			// TODO: "unknown", "ref", "token", etc
+			case "array":
+				// TODO: more completeness here (eg, non-object types)
+				structPtr := ""
+				if gen.Config.LegacyMode {
+					structPtr = "*"
+				}
+				return fmt.Sprintf("[]%s%s_%s_Elem", structPtr, gen.baseName(), strings.Title(v.Ref[1:])), nil
+			default: // presumed "object", "union"
+				if gen.Config.LegacyMode {
+					ptr = "*"
+				}
+				if v.Ref == "#main" {
+					return ptr + gen.baseName(), nil
+				}
+				return fmt.Sprintf("%s%s_%s", ptr, gen.baseName(), strings.Title(v.Ref[1:])), nil
+			}
+		}
+
+		// external reference
+		t, err := gen.externalRefType(v.Ref)
+		if err != nil {
+			return "", err
+		}
+		if gen.Config.LegacyMode {
+			ptr = "*"
+		}
+		return ptr + t, nil
+	default:
+		return "", fmt.Errorf("unhandled schema type in struct field: %T", def.Inner)
+	}
+}
+
+func (gen *CodeGenerator) externalRefType(ref string) (string, error) {
+	s, err := gen.Cat.Resolve(ref)
+	if err != nil {
+		return "", fmt.Errorf("could not resolve lexicon reference (%s): %w", ref, err)
+	}
+
+	switch s.Def.(type) {
+	case lexicon.SchemaString:
+		return "string", nil
+		// TODO: other concrete types and special-cases types, like arrays
+	}
+
+	parts := strings.SplitN(ref, "#", 3)
+	if len(parts) > 2 {
+		return "", fmt.Errorf("failed to parse external ref: %s", ref)
+	}
+	nsid, err := syntax.ParseNSID(parts[0])
+	if err != nil {
+		return "", fmt.Errorf("failed to parse external ref NSID (%s): %w", ref, err)
+	}
+
+	// check if this is actually in the same package (which might not mean the same NSID authority)
+	if nsidPkgName(nsid) == nsidPkgName(gen.Lex.NSID) {
+		if len(parts) == 1 || parts[1] == "main" {
+			return nsidBaseName(nsid), nil
+		} else {
+			return fmt.Sprintf("%s_%s", nsidBaseName(nsid), strings.Title(parts[1])), nil
+		}
+	}
+
+	if len(parts) == 1 || parts[1] == "main" {
+		return fmt.Sprintf("%s.%s", nsidPkgName(nsid), nsidBaseName(nsid)), nil
+	} else {
+		return fmt.Sprintf("%s.%s_%s", nsidPkgName(nsid), nsidBaseName(nsid), strings.Title(parts[1])), nil
+	}
+}
+
+func (gen *CodeGenerator) writeStruct(ft *FlatType, obj *lexicon.SchemaObject) error {
+
+	name := gen.baseName()
+	if ft.DefName != "main" {
+		name += "_" + strings.Title(ft.DefName)
+	}
+	for _, sub := range ft.Path {
+		name += "_" + strings.Title(sub)
+	}
+
+	if ft.DefName != "main" && len(ft.Path) == 0 {
+		fmt.Fprintf(gen.Out, "// %s is a \"%s\" in the %s schema.\n", name, ft.DefName, gen.Lex.NSID)
+		if obj.Description != nil {
+			fmt.Fprintln(gen.Out, "//")
+		}
+	}
+	if gen.Lex.Defs[ft.DefName].Type == "procedure" && len(ft.Path) == 1 && ft.Path[0] == "input" {
+		// TODO: "request body"
+		fmt.Fprintf(gen.Out, "// %s is the input argument to a %s call.\n", name, gen.Lex.NSID)
+	}
+	if (gen.Lex.Defs[ft.DefName].Type == "query" || gen.Lex.Defs[ft.DefName].Type == "procedure") && len(ft.Path) == 1 && ft.Path[0] == "output" {
+		// TODO: "response body"
+		fmt.Fprintf(gen.Out, "// %s is the output of a %s call.\n", name, gen.Lex.NSID)
+	}
+	skipDesc := false
+	if gen.Config.LegacyMode && ft.Type == "record" {
+		skipDesc = true
+	}
+	if obj.Description != nil && !skipDesc {
+		for _, l := range strings.Split(*obj.Description, "\n") {
+			fmt.Fprintf(gen.Out, "// %s\n", l)
+		}
+	}
+	fmt.Fprintf(gen.Out, "type %s struct {\n", name)
+
+	// iterate field in sorted order
+	fieldNames := []string{}
+	for fname := range obj.Properties {
+		fieldNames = append(fieldNames, fname)
+	}
+	sort.Strings(fieldNames)
+
+	// if this is a def-level struct, write out type decoder
+	skipType := false
+	if gen.Config.LegacyMode {
+		// TODO: skip $type for all defs in subscription. this isn't robust!
+		switch gen.Lex.MainType() {
+		case "subscription":
+			skipType = true
+		}
+	}
+	if len(ft.Path) == 0 && !skipType {
+		// TODO: can skip in some more situations?
+		fullName := gen.Lex.NSID.String()
+		if ft.DefName != "main" {
+			fullName += "#" + ft.DefName
+		}
+		omitempty := ",omitempty"
+		if ft.Type == "record" {
+			omitempty = ""
+		}
+		fmt.Fprintf(gen.Out, "    LexiconTypeID string `json:\"$type%s\" cborgen:\"$type,const=%s%s\"`\n", omitempty, fullName, omitempty)
+	}
+
+	for _, fname := range fieldNames {
+		field := obj.Properties[fname]
+		optional := false
+		omitempty := ""
+		if !isRequired(obj.Required, fname) {
+			optional = true
+			omitempty = ",omitempty"
+		}
+		if obj.IsNullable(fname) {
+			optional = true
+			omitempty = ""
+		}
+
+		var t string
+		var err error
+
+		switch v := field.Inner.(type) {
+		case lexicon.SchemaObject, lexicon.SchemaUnion:
+			t = name + "_" + strings.Title(fname)
+			if optional || gen.Config.LegacyMode {
+				t = "*" + t
+			}
+		case lexicon.SchemaArray:
+			switch v.Items.Inner.(type) {
+			case lexicon.SchemaObject, lexicon.SchemaUnion:
+				elemPtr := ""
+				if gen.Config.LegacyMode {
+					elemPtr = "*"
+				}
+				// NOTE: not using ptr for optional
+				t = fmt.Sprintf("[]%s%s_%s_Elem", elemPtr, name, strings.Title(fname))
+			default:
+				t, err = gen.fieldType(fname, &field, optional)
+				if err != nil {
+					return err
+				}
+			}
+		default:
+			t, err = gen.fieldType(fname, &field, optional)
+			if err != nil {
+				return err
+			}
+		}
+
+		if gen.Config.LegacyMode {
+			switch field.Inner.(type) {
+			case lexicon.SchemaBytes:
+				omitempty = ",omitempty"
+			}
+		}
+
+		cborExtra := ""
+		// HACK: copied from legacy code for now
+		if gen.Lex.NSID.String() == "com.atproto.label.defs" && name == "LabelDefs_SelfLabels" && fname == "values" {
+			cborExtra = ",preservenil"
+		}
+
+		desc := defDescription(&field)
+		if desc != "" {
+			fmt.Fprintf(gen.Out, "    // %s: %s\n", fname, desc)
+		}
+		fmt.Fprintf(gen.Out, "    %s %s", strings.ReplaceAll(strings.Title(fname), "-", ""), t)
+		fmt.Fprintf(gen.Out, " `json:\"%s%s\" cborgen:\"%s%s%s\"`\n", fname, omitempty, fname, omitempty, cborExtra)
+	}
+	fmt.Fprintf(gen.Out, "}\n\n")
+
+	return nil
+}
+
+type unionRef struct {
+	FieldName string
+	TypeName  string
+	LexName   string
+}
+
+func (gen *CodeGenerator) writeUnion(ft *FlatType, union *lexicon.SchemaUnion) error {
+
+	name := gen.baseName()
+	if ft.DefName != "main" {
+		name += "_" + strings.Title(ft.DefName)
+	}
+	for _, sub := range ft.Path {
+		name += "_" + strings.Title(sub)
+	}
+
+	unionRefs := map[string]unionRef{}
+	refNames := []string{}
+	for _, ref := range union.Refs {
+		r := unionRef{
+			LexName: ref,
+		}
+
+		if strings.HasPrefix(ref, "#") {
+			r.LexName = gen.Lex.NSID.String() + ref
+			n := gen.baseName()
+			if ref != "#main" {
+				n += "_" + strings.Title(ref[1:])
+			}
+			r.FieldName = n
+			r.TypeName = n
+		} else {
+			n, err := gen.externalRefType(ref)
+			if err != nil {
+				return err
+			}
+			r.FieldName = n
+			r.TypeName = n
+			if strings.Contains(n, ".") {
+				parts := strings.SplitN(n, ".", 2)
+				r.FieldName = parts[1]
+			}
+		}
+		refNames = append(refNames, r.FieldName)
+		unionRefs[r.FieldName] = r
+	}
+	if !gen.Config.LegacyMode {
+		sort.Strings(refNames)
+	}
+
+	// first print out the union struct type
+	if union.Description != nil {
+		for _, l := range strings.Split(*union.Description, "\n") {
+			fmt.Fprintf(gen.Out, "// %s\n", l)
+		}
+	}
+	fmt.Fprintf(gen.Out, "type %s struct {\n", name)
+	for _, rname := range refNames {
+		ref := unionRefs[rname]
+		fmt.Fprintf(gen.Out, "    %s *%s\n", ref.FieldName, ref.TypeName)
+	}
+	fmt.Fprintf(gen.Out, "}\n\n")
+
+	// ... then MarshalJSON
+	fmt.Fprintf(gen.Out, "func (t *%s) MarshalJSON() ([]byte, error) {\n", name)
+	for _, rname := range refNames {
+		ref := unionRefs[rname]
+		fmt.Fprintf(gen.Out, "    if t.%s != nil {\n", ref.FieldName)
+		fmt.Fprintf(gen.Out, "        t.%s.LexiconTypeID = \"%s\"\n", ref.FieldName, ref.LexName)
+		fmt.Fprintf(gen.Out, "        return json.Marshal(t.%s)\n", ref.FieldName)
+		fmt.Fprintf(gen.Out, "    }\n")
+	}
+	fmt.Fprintf(gen.Out, "    return nil, fmt.Errorf(\"can not marshal empty union as JSON\")")
+	fmt.Fprintf(gen.Out, "}\n\n")
+
+	// ... then UnmarshalJSON
+	fmt.Fprintf(gen.Out, "func (t *%s) UnmarshalJSON(b []byte) error {\n", name)
+	fmt.Fprintf(gen.Out, "    typ, err := lexutil.TypeExtract(b)\n")
+	fmt.Fprintf(gen.Out, "    if err != nil {\n")
+	fmt.Fprintf(gen.Out, "        return err\n")
+	fmt.Fprintf(gen.Out, "    }\n\n")
+	fmt.Fprintf(gen.Out, "    switch typ {\n")
+	for _, rname := range refNames {
+		ref := unionRefs[rname]
+		fmt.Fprintf(gen.Out, "    case \"%s\":\n", ref.LexName)
+		fmt.Fprintf(gen.Out, "        t.%s = new(%s)\n", ref.FieldName, ref.TypeName)
+		fmt.Fprintf(gen.Out, "        return json.Unmarshal(b, t.%s)\n", ref.FieldName)
+	}
+	fmt.Fprintf(gen.Out, "    default:\n")
+	if union.Closed != nil && *union.Closed {
+		// TODO: better error message
+		fmt.Fprintf(gen.Out, "        return fmt.Errorf(\"closed unions must match a listed schema\")\n")
+	} else {
+		fmt.Fprintf(gen.Out, "        return nil\n")
+	}
+	fmt.Fprintf(gen.Out, "    }\n")
+	fmt.Fprintf(gen.Out, "}\n\n")
+
+	// only import CBOR marshalling of unions in legacy mode
+	if !gen.Config.LegacyMode {
+		return nil
+	}
+
+	switch gen.Lex.MainType() {
+	case "record", "subscription":
+		// no-op
+	case "object":
+		// hacks for legacy serialization
+		nsid := gen.Lex.NSID.String()
+		if !(nsid == "app.bsky.richtext.facet" || (nsid == "app.bsky.embed.recordWithMedia" && ft.DefName == "main")) {
+			return nil
+		}
+	default:
+		return nil
+	}
+
+	// ... then MarshalCBOR
+	fmt.Fprintf(gen.Out, "func (t *%s) MarshalCBOR(w io.Writer) error {\n\n", name)
+	fmt.Fprintf(gen.Out, "    if t == nil {\n")
+	fmt.Fprintf(gen.Out, "        _, err := w.Write(cbg.CborNull)\n")
+	fmt.Fprintf(gen.Out, "        return err")
+	fmt.Fprintf(gen.Out, "    }\n")
+	for _, rname := range refNames {
+		ref := unionRefs[rname]
+		fmt.Fprintf(gen.Out, "    if t.%s != nil {\n", ref.FieldName)
+		fmt.Fprintf(gen.Out, "        return t.%s.MarshalCBOR(w)\n", ref.FieldName)
+		fmt.Fprintf(gen.Out, "    }\n")
+	}
+	fmt.Fprintf(gen.Out, "    return fmt.Errorf(\"can not marshal empty union as CBOR\")")
+	fmt.Fprintf(gen.Out, "}\n\n")
+
+	// ... then UnmarshalCBOR
+	fmt.Fprintf(gen.Out, "func (t *%s) UnmarshalCBOR(r io.Reader) error {\n", name)
+	fmt.Fprintf(gen.Out, "    typ, b, err := lexutil.CborTypeExtractReader(r)\n")
+	fmt.Fprintf(gen.Out, "    if err != nil {\n")
+	fmt.Fprintf(gen.Out, "        return err\n")
+	fmt.Fprintf(gen.Out, "    }\n\n")
+	fmt.Fprintf(gen.Out, "    switch typ {\n")
+	for _, rname := range refNames {
+		ref := unionRefs[rname]
+		fmt.Fprintf(gen.Out, "    case \"%s\":\n", ref.LexName)
+		fmt.Fprintf(gen.Out, "        t.%s = new(%s)\n", ref.FieldName, ref.TypeName)
+		fmt.Fprintf(gen.Out, "        return t.%s.UnmarshalCBOR(bytes.NewReader(b))\n", ref.FieldName)
+	}
+	fmt.Fprintf(gen.Out, "    default:\n")
+	fmt.Fprintf(gen.Out, "        return nil\n")
+	fmt.Fprintf(gen.Out, "    }\n")
+	fmt.Fprintf(gen.Out, "}\n\n")
+
+	return nil
+}
+
+func (gen *CodeGenerator) writeEndpoint(ft *FlatType, desc string, params *lexicon.SchemaParams, output, input *lexicon.SchemaBody, isProcedure bool) error {
+	name := gen.baseName()
+
+	fmt.Fprintf(gen.Out, "// %s calls the XRPC method \"%s\".\n", name, gen.Lex.NSID)
+	if desc != "" && !gen.Config.LegacyMode {
+		fmt.Fprintln(gen.Out, "//")
+		for _, l := range strings.Split(desc, "\n") {
+			fmt.Fprintf(gen.Out, "// %s\n", l)
+		}
+	}
+
+	outputBytes := false
+	outputStruct := ""
+	if output != nil && output.Schema != nil {
+		switch v := output.Schema.Inner.(type) {
+		case lexicon.SchemaObject, lexicon.SchemaUnion:
+			outputStruct = name + "_Output"
+		case lexicon.SchemaRef:
+			if strings.HasPrefix(v.Ref, "#") {
+				// local reference
+				outputStruct = fmt.Sprintf("%s_%s", gen.baseName(), strings.Title(v.Ref[1:]))
+			} else {
+				// external reference
+				t, err := gen.externalRefType(v.Ref)
+				if err != nil {
+					return err
+				}
+				outputStruct = t
+			}
+		default:
+			return fmt.Errorf("unsupported endpoint output schema def type: %T", output.Schema.Inner)
+		}
+	} else if output != nil && output.Encoding != "" {
+		outputBytes = true
+	}
+
+	paramNames := []string{}
+	if params != nil {
+		for name := range params.Properties {
+			paramNames = append(paramNames, name)
+		}
+	}
+	sort.Strings(paramNames)
+
+	args := []string{"ctx context.Context", "c lexutil.LexClient"}
+	reqParams := []string{}
+	optParams := []string{}
+	if len(paramNames) > 0 {
+		fmt.Fprintln(gen.Out, "//")
+		for _, name := range paramNames {
+			param := params.Properties[name]
+			ptr := "*"
+			if isRequired(params.Required, name) {
+				ptr = ""
+				reqParams = append(reqParams, name)
+			} else {
+				optParams = append(optParams, name)
+			}
+			switch v := param.Inner.(type) {
+			case lexicon.SchemaBoolean:
+				if v.Description != nil && *v.Description != "" {
+					fmt.Fprintf(gen.Out, "// %s: %s\n", name, *v.Description)
+				}
+				if gen.Config.LegacyMode {
+					ptr = ""
+				}
+				args = append(args, fmt.Sprintf("%s %sbool", name, ptr))
+			case lexicon.SchemaInteger:
+				if v.Description != nil && *v.Description != "" {
+					fmt.Fprintf(gen.Out, "// %s: %s\n", name, *v.Description)
+				}
+				if gen.Config.LegacyMode {
+					ptr = ""
+				}
+				args = append(args, fmt.Sprintf("%s %sint64", name, ptr))
+			case lexicon.SchemaString:
+				if v.Description != nil && *v.Description != "" {
+					fmt.Fprintf(gen.Out, "// %s: %s\n", name, *v.Description)
+				}
+				args = append(args, fmt.Sprintf("%s string", name))
+			case lexicon.SchemaUnknown:
+				if v.Description != nil && *v.Description != "" {
+					fmt.Fprintf(gen.Out, "// %s: %s\n", name, *v.Description)
+				}
+				args = append(args, fmt.Sprintf("%s any", name))
+			case lexicon.SchemaArray:
+				if v.Description != nil && *v.Description != "" {
+					suffix := "[]"
+					if gen.Config.LegacyMode {
+						suffix = ""
+					}
+					fmt.Fprintf(gen.Out, "// %s%s: %s\n", name, suffix, *v.Description)
+				}
+				switch v.Items.Inner.(type) {
+				case lexicon.SchemaBoolean:
+					args = append(args, fmt.Sprintf("%s []bool", name))
+				case lexicon.SchemaInteger:
+					args = append(args, fmt.Sprintf("%s []int64", name))
+				case lexicon.SchemaString:
+					args = append(args, fmt.Sprintf("%s []string", name))
+				default:
+					return fmt.Errorf("unsupported parameter array type: %T", param.Inner)
+				}
+			default:
+				return fmt.Errorf("unsupported parameter type: %T", param.Inner)
+			}
+		}
+	}
+
+	inputArg := "nil"
+	inputEncoding := ""
+	inputStruct := ""
+	if isProcedure && input != nil && input.Schema != nil {
+		inputArg = "input"
+		inputEncoding = input.Encoding
+		switch v := input.Schema.Inner.(type) {
+		case lexicon.SchemaObject, lexicon.SchemaUnion:
+			inputStruct = name + "_Input"
+		case lexicon.SchemaRef:
+			if strings.HasPrefix(v.Ref, "#") {
+				// local reference
+				inputStruct = fmt.Sprintf("%s_%s", gen.baseName(), strings.Title(v.Ref[1:]))
+			} else {
+				// external reference
+				t, err := gen.externalRefType(v.Ref)
+				if err != nil {
+					return err
+				}
+				inputStruct = t
+			}
+		}
+		args = append(args, fmt.Sprintf("input *%s", inputStruct))
+	} else if isProcedure && input != nil && input.Encoding != "" {
+		inputArg = "input"
+		inputEncoding = input.Encoding
+		args = append(args, "input io.Reader")
+	}
+
+	doOutParam := ""
+	returnType := ""
+	fmt.Fprintf(gen.Out, "func %s(%s) ", name, strings.Join(args, ", "))
+	if outputStruct != "" {
+		fmt.Fprintf(gen.Out, "(*%s, error) {\n", outputStruct)
+		fmt.Fprintf(gen.Out, "    var out %s\n", outputStruct)
+		if !gen.Config.LegacyMode {
+			fmt.Fprintln(gen.Out, "")
+		}
+		doOutParam = "&out"
+		returnType = "&out"
+	} else if outputBytes {
+		fmt.Fprintf(gen.Out, "([]byte, error) {\n")
+		fmt.Fprintf(gen.Out, "    buf := new(bytes.Buffer)\n\n")
+		doOutParam = "buf"
+		returnType = "buf.Bytes()"
+	} else {
+		fmt.Fprintf(gen.Out, "error {\n")
+		doOutParam = "nil"
+	}
+	paramsArg := "nil"
+	if params != nil && len(params.Properties) > 0 {
+		paramsArg = "params"
+		if gen.Config.LegacyMode {
+			fmt.Fprintln(gen.Out, "")
+		}
+		// TODO: switch to map[string]any
+		fmt.Fprintf(gen.Out, "    params := map[string]interface{}{}\n")
+	}
+	for _, name := range optParams {
+		param := params.Properties[name]
+		switch param.Inner.(type) {
+		case lexicon.SchemaString:
+			fmt.Fprintf(gen.Out, "    if %s != \"\" {\n", name)
+			fmt.Fprintf(gen.Out, "        params[\"%s\"] = %s\n", name, name)
+			fmt.Fprintf(gen.Out, "    }\n")
+		case lexicon.SchemaArray:
+			fmt.Fprintf(gen.Out, "    if len(%s) != 0 {\n", name)
+			fmt.Fprintf(gen.Out, "        params[\"%s\"] = %s\n", name, name)
+			fmt.Fprintf(gen.Out, "    }\n")
+		case lexicon.SchemaUnknown:
+			fmt.Fprintf(gen.Out, "    if %s != nil {\n", name)
+			fmt.Fprintf(gen.Out, "        params[\"%s\"] = %s\n", name, name)
+			fmt.Fprintf(gen.Out, "    }\n")
+		case lexicon.SchemaInteger:
+			if gen.Config.LegacyMode {
+				fmt.Fprintf(gen.Out, "    if %s != 0 {\n", name)
+				fmt.Fprintf(gen.Out, "        params[\"%s\"] = %s\n", name, name)
+				fmt.Fprintf(gen.Out, "    }\n")
+			} else {
+				fmt.Fprintf(gen.Out, "    if %s != nil {\n", name)
+				fmt.Fprintf(gen.Out, "        params[\"%s\"] = *%s\n", name, name)
+				fmt.Fprintf(gen.Out, "    }\n")
+			}
+		case lexicon.SchemaBoolean:
+			if gen.Config.LegacyMode {
+				fmt.Fprintf(gen.Out, "    if %s {\n", name)
+				fmt.Fprintf(gen.Out, "        params[\"%s\"] = %s\n", name, name)
+				fmt.Fprintf(gen.Out, "    }\n")
+			} else {
+				fmt.Fprintf(gen.Out, "    if %s != nil {\n", name)
+				fmt.Fprintf(gen.Out, "        params[\"%s\"] = *%s\n", name, name)
+				fmt.Fprintf(gen.Out, "    }\n")
+			}
+		default:
+			fmt.Fprintf(gen.Out, "    if %s != nil {\n", name)
+			fmt.Fprintf(gen.Out, "        params[\"%s\"] = *%s\n", name, name)
+			fmt.Fprintf(gen.Out, "    }\n")
+		}
+	}
+	for _, name := range reqParams {
+		fmt.Fprintf(gen.Out, "    params[\"%s\"] = %s\n", name, name)
+	}
+	if !gen.Config.LegacyMode {
+		fmt.Fprintln(gen.Out, "")
+	}
+
+	method := "lexutil.Query"
+	if isProcedure {
+		method = "lexutil.Procedure"
+	}
+
+	fmt.Fprintf(gen.Out, "    if err := c.LexDo(ctx, %s, \"%s\", \"%s\", %s, %s, %s); err != nil {\n", method, inputEncoding, gen.Lex.NSID, paramsArg, inputArg, doOutParam)
+	if returnType != "" {
+		fmt.Fprintf(gen.Out, "        return nil, err\n")
+	} else {
+		fmt.Fprintf(gen.Out, "        return err\n")
+	}
+	fmt.Fprintf(gen.Out, "    }\n")
+	if gen.Config.LegacyMode {
+		fmt.Fprintln(gen.Out, "")
+	}
+	if returnType != "" {
+		fmt.Fprintf(gen.Out, "    return %s, nil\n", returnType)
+	} else {
+		fmt.Fprintf(gen.Out, "    return nil\n")
+	}
+	fmt.Fprintf(gen.Out, "}\n\n")
+
+	return nil
+}
diff --git a/lex/lexgen/doc.go b/lex/lexgen/doc.go
new file mode 100644
index 00000000..2133f81e
--- /dev/null
+++ b/lex/lexgen/doc.go
@@ -0,0 +1,17 @@
+/*
+Package implementing Go code generation for lexicon schemas.
+
+Used by the 'lexgen' CLI tool to output Go structs and client API helpers based on Lexicon schemas. This package currently includes a "legacy" mode to stay as close as possible to the previous code generation output.
+
+WARNING: this package is still a work in progress. Both the package API and the generated code are likely to change, possibly in backwards-incompatible ways.
+
+# Package Structure
+
+The package works in two steps:
+
+- "flattening" parses a full lexicon schema file and copies nested type definitions in to a top-level array
+- code generation outputs a single Go source code file corresponding to a flattened lexicon schema file
+
+Wrapping code is expected to handle code formatting and fixing imports (which mostly means removing unused imports).
+*/
+package lexgen
diff --git a/lex/lexgen/flatten.go b/lex/lexgen/flatten.go
new file mode 100644
index 00000000..37c915d3
--- /dev/null
+++ b/lex/lexgen/flatten.go
@@ -0,0 +1,242 @@
+package lexgen
+
+import (
+	"fmt"
+	"slices"
+	"sort"
+	"strings"
+
+	"github.com/bluesky-social/indigo/atproto/lexicon"
+	"github.com/bluesky-social/indigo/atproto/syntax"
+)
+
+// Intermediate representation of a complete lexicon schema file, containing one or more definitions.
+type FlatLexicon struct {
+	NSID         syntax.NSID
+	Description  *string
+	ExternalRefs map[string]bool // NSID with optional ref
+	Defs         map[string]FlatDef
+	Types        []FlatType
+}
+
+// Minimal context about an individual top-level schema definition: just the short name and schema type.
+type FlatDef struct {
+	Name string
+	Type string
+}
+
+// An individual "type definition", which is a small unit of schema definition that corresponds to a named unit of generated code. For example, a struct or API endpoint.
+type FlatType struct {
+	// the short name of the schema def that this type is under
+	DefName string
+	Path    []string
+	Type    string
+	Schema  *lexicon.SchemaDef
+}
+
+func FlattenSchemaFile(sf *lexicon.SchemaFile) (*FlatLexicon, error) {
+	nsid, err := syntax.ParseNSID(sf.ID)
+	if err != nil {
+		return nil, err
+	}
+
+	fl := FlatLexicon{
+		NSID:         nsid,
+		Description:  sf.Description,
+		ExternalRefs: map[string]bool{},
+		Defs:         map[string]FlatDef{},
+		Types:        []FlatType{},
+	}
+
+	// iterate defs in sorted order; except "main" is always first if present
+	defNames := []string{}
+	hasMain := false
+	for name := range sf.Defs {
+		if name == "main" {
+			hasMain = true
+			continue
+		}
+		defNames = append(defNames, name)
+	}
+	sort.Strings(defNames)
+	if hasMain {
+		defNames = append([]string{"main"}, defNames...)
+	}
+
+	for _, name := range defNames {
+		def := sf.Defs[name]
+		if err := fl.flattenDef(name, &def); err != nil {
+			return nil, err
+		}
+	}
+
+	return &fl, nil
+}
+
+func (fl *FlatLexicon) flattenDef(name string, def *lexicon.SchemaDef) error {
+
+	t, err := defType(def)
+	if err != nil {
+		return err
+	}
+
+	fd := FlatDef{
+		Name: name,
+		Type: t,
+	}
+	fl.Defs[name] = fd
+
+	return fl.flattenType(&fd, []string{}, def)
+}
+
+func (fl *FlatLexicon) flattenType(fd *FlatDef, tpath []string, def *lexicon.SchemaDef) error {
+
+	t, err := defType(def)
+	if err != nil {
+		return err
+	}
+
+	ft := FlatType{
+		DefName: fd.Name,
+		Path:    slices.Clone(tpath),
+		Type:    t,
+		Schema:  def,
+	}
+
+	switch v := def.Inner.(type) {
+	case lexicon.SchemaRecord:
+		fl.Types = append(fl.Types, ft)
+		if err := fl.flattenObject(fd, tpath, &v.Record); err != nil {
+			return err
+		}
+	case lexicon.SchemaQuery:
+		// v.Properties: only boolean, integer, string, or unknown are allowed, so recursion not really needed?
+		if v.Output != nil && v.Output.Schema != nil {
+			tp := slices.Clone(tpath)
+			tp = append(tp, "output")
+			if err := fl.flattenType(fd, tp, v.Output.Schema); err != nil {
+				return err
+			}
+		}
+		fl.Types = append(fl.Types, ft)
+	case lexicon.SchemaProcedure:
+		// v.Properties: same as above
+		if v.Input != nil && v.Input.Schema != nil {
+			tp := slices.Clone(tpath)
+			tp = append(tp, "input")
+			if err := fl.flattenType(fd, tp, v.Input.Schema); err != nil {
+				return err
+			}
+		}
+		if v.Output != nil && v.Output.Schema != nil {
+			tp := slices.Clone(tpath)
+			tp = append(tp, "output")
+			if err := fl.flattenType(fd, tp, v.Output.Schema); err != nil {
+				return err
+			}
+		}
+		fl.Types = append(fl.Types, ft)
+	case lexicon.SchemaSubscription:
+		// v.Properties: same as above
+		if v.Message != nil {
+			for _, ref := range v.Message.Schema.Refs {
+				if !strings.HasPrefix(ref, "#") {
+					fl.ExternalRefs[strings.TrimSuffix(ref, "#main")] = true
+				}
+			}
+		}
+		fl.Types = append(fl.Types, ft)
+	case lexicon.SchemaObject:
+		fl.Types = append(fl.Types, ft)
+		if err := fl.flattenObject(fd, tpath, &v); err != nil {
+			return err
+		}
+	case lexicon.SchemaRef:
+		if !strings.HasPrefix(v.Ref, "#") {
+			fl.ExternalRefs[strings.TrimSuffix(v.Ref, "#main")] = true
+		}
+		fl.Types = append(fl.Types, ft)
+	case lexicon.SchemaUnion:
+		for _, ref := range v.Refs {
+			if !strings.HasPrefix(ref, "#") {
+				fl.ExternalRefs[strings.TrimSuffix(ref, "#main")] = true
+			}
+		}
+		fl.Types = append(fl.Types, ft)
+	case lexicon.SchemaArray:
+		// flatten the inner item
+		tp := slices.Clone(tpath)
+		tp = append(tp, "elem")
+		if err := fl.flattenType(fd, tpath, &v.Items); err != nil {
+			return err
+		}
+		// don't emit the array itself
+		return nil
+	case lexicon.SchemaString, lexicon.SchemaInteger, lexicon.SchemaBoolean, lexicon.SchemaUnknown, lexicon.SchemaBytes:
+		// don't emit
+		// NOTE: might want to emit some string "knownValue" lists in the future?
+	case lexicon.SchemaCIDLink, lexicon.SchemaBlob:
+		// don't emit
+	case lexicon.SchemaToken:
+		// pass-through (emit)
+		fl.Types = append(fl.Types, ft)
+	case lexicon.SchemaPermissionSet, lexicon.SchemaPermission:
+		// pass-through (emit)
+		fl.Types = append(fl.Types, ft)
+	default:
+		return fmt.Errorf("unsupported def type for flattening (%s): %T", fd.Name, def.Inner)
+	}
+
+	return nil
+}
+
+func (fl *FlatLexicon) flattenObject(fd *FlatDef, tpath []string, obj *lexicon.SchemaObject) error {
+
+	keys := []string{}
+	for n := range obj.Properties {
+		keys = append(keys, n)
+	}
+	sort.Strings(keys)
+
+	for _, fname := range keys {
+		field := obj.Properties[fname]
+		tp := slices.Clone(tpath)
+		tp = append(tp, fname)
+		switch v := field.Inner.(type) {
+		case lexicon.SchemaBoolean, lexicon.SchemaInteger, lexicon.SchemaString, lexicon.SchemaBytes:
+			// no-op
+		case lexicon.SchemaCIDLink, lexicon.SchemaBlob, lexicon.SchemaUnknown:
+			// no-op, but maybe set a flag on def?
+		case lexicon.SchemaArray:
+			tp = append(tp, "elem")
+			if err := fl.flattenType(fd, tp, &v.Items); err != nil {
+				return err
+			}
+		case lexicon.SchemaObject:
+			if err := fl.flattenType(fd, tp, &field); err != nil {
+				return err
+			}
+		case lexicon.SchemaRef:
+			if !strings.HasPrefix(v.Ref, "#") {
+				// remove any #main suffix
+				fl.ExternalRefs[strings.TrimSuffix(v.Ref, "#main")] = true
+			}
+		case lexicon.SchemaUnion:
+			if err := fl.flattenType(fd, tp, &field); err != nil {
+				return err
+			}
+		default:
+			return fmt.Errorf("unsupported field type for object flattening: %T", field.Inner)
+		}
+	}
+	return nil
+}
+
+// Returns the type of any "#main" definition in this file (or else an empty string)
+func (fl *FlatLexicon) MainType() string {
+	main, ok := fl.Defs["main"]
+	if !ok {
+		return ""
+	}
+	return main.Type
+}
diff --git a/lex/lexgen/util.go b/lex/lexgen/util.go
new file mode 100644
index 00000000..f6770fe4
--- /dev/null
+++ b/lex/lexgen/util.go
@@ -0,0 +1,156 @@
+package lexgen
+
+import (
+	"fmt"
+	"slices"
+	"strings"
+
+	"github.com/bluesky-social/indigo/atproto/lexicon"
+	"github.com/bluesky-social/indigo/atproto/syntax"
+
+	"golang.org/x/net/publicsuffix"
+)
+
+func defType(sd *lexicon.SchemaDef) (string, error) {
+	switch sd.Inner.(type) {
+	case lexicon.SchemaRecord:
+		return "record", nil
+	case lexicon.SchemaQuery:
+		return "query", nil
+	case lexicon.SchemaProcedure:
+		return "procedure", nil
+	case lexicon.SchemaSubscription:
+		return "subscription", nil
+	case lexicon.SchemaPermissionSet:
+		return "permission-set", nil
+	case lexicon.SchemaPermission:
+		return "permission", nil
+	case lexicon.SchemaBoolean:
+		return "boolean", nil
+	case lexicon.SchemaInteger:
+		return "integer", nil
+	case lexicon.SchemaString:
+		return "string", nil
+	case lexicon.SchemaBytes:
+		return "bytes", nil
+	case lexicon.SchemaCIDLink:
+		return "cid-link", nil
+	case lexicon.SchemaArray:
+		return "array", nil
+	case lexicon.SchemaObject:
+		return "object", nil
+	case lexicon.SchemaBlob:
+		return "blob", nil
+	case lexicon.SchemaParams:
+		return "params", nil
+	case lexicon.SchemaToken:
+		return "token", nil
+	case lexicon.SchemaRef:
+		return "ref", nil
+	case lexicon.SchemaUnion:
+		return "union", nil
+	case lexicon.SchemaUnknown:
+		return "unknown", nil
+	default:
+		return "", fmt.Errorf("unhandled schema type: %T", sd.Inner)
+	}
+}
+
+func defDescription(sd *lexicon.SchemaDef) string {
+	var desc *string
+
+	switch v := sd.Inner.(type) {
+	case lexicon.SchemaRecord:
+		desc = v.Description
+	case lexicon.SchemaQuery:
+		desc = v.Description
+	case lexicon.SchemaProcedure:
+		desc = v.Description
+	case lexicon.SchemaSubscription:
+		desc = v.Description
+	case lexicon.SchemaPermissionSet:
+		desc = v.Description
+	case lexicon.SchemaPermission:
+		desc = v.Description
+	case lexicon.SchemaBoolean:
+		desc = v.Description
+	case lexicon.SchemaInteger:
+		desc = v.Description
+	case lexicon.SchemaString:
+		desc = v.Description
+	case lexicon.SchemaBytes:
+		desc = v.Description
+	case lexicon.SchemaCIDLink:
+		desc = v.Description
+	case lexicon.SchemaArray:
+		desc = v.Description
+	case lexicon.SchemaObject:
+		desc = v.Description
+	case lexicon.SchemaBlob:
+		desc = v.Description
+	case lexicon.SchemaParams:
+		desc = v.Description
+	case lexicon.SchemaToken:
+		desc = v.Description
+	case lexicon.SchemaRef:
+		desc = v.Description
+	case lexicon.SchemaUnion:
+		desc = v.Description
+	case lexicon.SchemaUnknown:
+		desc = v.Description
+	}
+	if desc != nil && *desc != "" {
+		return *desc
+	}
+	return ""
+}
+
+func isCompoundDef(sd *lexicon.SchemaDef) bool {
+	switch sd.Inner.(type) {
+	case lexicon.SchemaRecord, lexicon.SchemaQuery, lexicon.SchemaProcedure, lexicon.SchemaSubscription, lexicon.SchemaArray, lexicon.SchemaObject, lexicon.SchemaUnion:
+		return true
+	default:
+		return false
+	}
+}
+
+func nsidPkgName(nsid syntax.NSID) string {
+	domain := strings.ToLower(nsid.Authority())
+	reg, err := publicsuffix.EffectiveTLDPlusOne(domain)
+	if err != nil {
+		return "FAIL"
+	}
+	parts := strings.Split(reg, ".")
+	slices.Reverse(parts)
+
+	return strings.Join(parts, "")
+}
+
+func nsidBaseName(nsid syntax.NSID) string {
+	domain := strings.ToLower(nsid.Authority())
+	reg, err := publicsuffix.EffectiveTLDPlusOne(domain)
+	if err != nil {
+		return "FAIL"
+	}
+	rem := domain[0 : len(domain)-len(reg)]
+	parts := strings.Split(rem, ".")
+	slices.Reverse(parts)
+	parts = append(parts, nsid.Name())
+	for i := range parts {
+		parts[i] = strings.Title(parts[i])
+	}
+	return strings.Join(parts, "")
+}
+
+func nsidFileName(nsid syntax.NSID) string {
+	domain := strings.ToLower(nsid.Authority())
+	reg, err := publicsuffix.EffectiveTLDPlusOne(domain)
+	if err != nil {
+		return "FAIL"
+	}
+	rem := domain[0 : len(domain)-len(reg)]
+	parts := strings.Split(rem, ".")
+	slices.Reverse(parts)
+	parts = append(parts, nsid.Name())
+	return strings.Join(parts, "")
+}
diff --git a/lex/lexlint/breaking.go b/lex/lexlint/breaking.go
new file mode 100644
index 00000000..3d82e84c
--- /dev/null
+++ b/lex/lexlint/breaking.go
@@ -0,0 +1,372 @@
+package lexlint
+
+import (
+	"fmt"
+	"log/slog"
+	"reflect"
+	"sort"
+
+	"github.com/bluesky-social/indigo/atproto/lexicon"
+	"github.com/bluesky-social/indigo/atproto/syntax"
+)
+
+func BreakingChanges(before, after *lexicon.SchemaFile) []LintIssue {
+	return breakingMaps(syntax.NSID(before.ID), before.Defs, after.Defs)
+}
+
+func breakingMaps(nsid syntax.NSID, localMap, remoteMap map[string]lexicon.SchemaDef) []LintIssue {
+	issues := []LintIssue{}
+
+	// TODO: maybe only care about the intersection of keys, not union?
+	keyMap := map[string]bool{}
+	for k := range localMap {
+		keyMap[k] = true
+	}
+	for k := range remoteMap {
+		keyMap[k] = true
+	}
+	keys := []string{}
+	for k := range keyMap {
+		keys = append(keys, k)
+	}
+	sort.Strings(keys)
+
+	for _, k := range keys {
+		// NOTE: adding or removing an entire definition or sub-object doesn't break anything
+		local, ok := localMap[k]
+		if !ok {
+			continue
+		}
+		remote, ok := remoteMap[k]
+		if !ok {
+			continue
+		}
+
+		nestIssues := breakingDefs(nsid, k, local, remote)
+		if len(nestIssues) > 0 {
+			issues = append(issues, nestIssues...)
+		}
+	}
+
+	return issues
+}
+
+func breakingDefs(nsid syntax.NSID, name string, local, remote lexicon.SchemaDef) []LintIssue {
+	issues := []LintIssue{}
+
+	// NOTE: in some situations this sort of change might actually be allowed?
+	if reflect.TypeOf(local) != reflect.TypeOf(remote) {
+		issues = append(issues, LintIssue{
+			NSID:            nsid,
+			LintLevel:       "error",
+			LintName:        "type-change",
+			LintDescription: "schema definition type changed",
+			Message:         fmt.Sprintf("schema type changed (%s): %T != %T", name, local, remote),
+		})
+		return issues
+	}
+
+	switch l := local.Inner.(type) {
+	case lexicon.SchemaRecord:
+		slog.Debug("checking record", "name", name, "nsid", nsid)
+		r := remote.Inner.(lexicon.SchemaRecord)
+		if l.Key != r.Key {
+			issues = append(issues, LintIssue{
+				NSID:            nsid,
+				LintLevel:       "error",
+				LintName:        "record-key-type",
+				LintDescription: "record key type changed",
+				Message:         fmt.Sprintf("schema type changed (%s): %s != %s", name, l.Key, r.Key),
+			})
+		}
+		issues = append(issues, breakingDefs(nsid, name, lexicon.SchemaDef{Inner: l.Record}, lexicon.SchemaDef{Inner: r.Record})...)
+	case lexicon.SchemaQuery:
+		r := remote.Inner.(lexicon.SchemaQuery)
+		// TODO: situation where overall parameters added/removed, and required fields involved
+		if l.Parameters != nil && r.Parameters != nil {
+			issues = append(issues, breakingDefs(nsid, name, lexicon.SchemaDef{Inner: *l.Parameters}, lexicon.SchemaDef{Inner: *r.Parameters})...)
+		}
+		// TODO: situation where output requirement changes
+		if l.Output != nil && r.Output != nil {
+			issues = append(issues, breakingDefs(nsid, name, lexicon.SchemaDef{Inner: *l.Output}, lexicon.SchemaDef{Inner: *r.Output})...)
+		}
+		// TODO: do Errors matter?
+	case lexicon.SchemaProcedure:
+		r := remote.Inner.(lexicon.SchemaProcedure)
+		// TODO: situation where overall parameters added/removed, and required fields involved
+		if l.Parameters != nil && r.Parameters != nil {
+			issues = append(issues, breakingDefs(nsid, name, lexicon.SchemaDef{Inner: *l.Parameters}, lexicon.SchemaDef{Inner: *r.Parameters})...)
+		}
+		// TODO: situation where output requirement changes
+		if l.Input != nil && r.Input != nil {
+			issues = append(issues, breakingDefs(nsid, name, lexicon.SchemaDef{Inner: *l.Input}, lexicon.SchemaDef{Inner: *r.Input})...)
+		}
+		// TODO: situation where output requirement changes
+		if l.Output != nil && r.Output != nil {
+			issues = append(issues, breakingDefs(nsid, name, lexicon.SchemaDef{Inner: *l.Output}, lexicon.SchemaDef{Inner: *r.Output})...)
+		}
+		// TODO: do Errors matter?
+	// TODO: lexicon.SchemaSubscription (and SchemaMessage)
+	case lexicon.SchemaBody:
+		r := remote.Inner.(lexicon.SchemaBody)
+		if l.Encoding != r.Encoding {
+			issues = append(issues, LintIssue{
+				NSID:            nsid,
+				LintLevel:       "error",
+				LintName:        "body-encoding",
+				LintDescription: "API endpoint body content type (encoding) changed",
+				Message:         fmt.Sprintf("body encoding changed (%s): %s != %s", name, l.Encoding, r.Encoding),
+			})
+		}
+		if l.Schema != nil && r.Schema != nil {
+			issues = append(issues, breakingDefs(nsid, name, *l.Schema, *r.Schema)...)
+		}
+	case lexicon.SchemaBoolean:
+		r := remote.Inner.(lexicon.SchemaBoolean)
+		// NOTE: default can change safely
+		if !eqOptBool(l.Const, r.Const) {
+			issues = append(issues, LintIssue{
+				NSID:            nsid,
+				LintLevel:       "error",
+				LintName:        "const-value",
+				LintDescription: "schema const value change",
+				Message:         fmt.Sprintf("const value changed (%s): %v != %v", name, l.Const, r.Const),
+			})
+		}
+	case lexicon.SchemaInteger:
+		r := remote.Inner.(lexicon.SchemaInteger)
+		// NOTE: default can change safely
+		if !eqOptInt(l.Const, r.Const) {
+			issues = append(issues, LintIssue{
+				NSID:            nsid,
+				LintLevel:       "error",
+				LintName:        "const-value",
+				LintDescription: "schema const value change",
+				Message:         fmt.Sprintf("const value changed (%s): %v != %v", name, l.Const, r.Const),
+			})
+		}
+		sort.Ints(l.Enum)
+		sort.Ints(r.Enum)
+		if !reflect.DeepEqual(l.Enum, r.Enum) {
+			issues = append(issues, LintIssue{
+				NSID:            nsid,
+				LintLevel:       "error",
+				LintName:        "enum-values",
+				LintDescription: "schema enum values change",
+				Message:         fmt.Sprintf("integer enum value changed (%s)", name),
+			})
+		}
+		if !eqOptInt(l.Minimum, r.Minimum) || !eqOptInt(l.Maximum, r.Maximum) {
+			issues = append(issues, LintIssue{
+				NSID:            nsid,
+				LintLevel:       "warn",
+				LintName:        "integer-range",
+				LintDescription: "schema min/max values change",
+				Message:         fmt.Sprintf("integer min/max values changed (%s)", name),
+			})
+		}
+	case lexicon.SchemaString:
+		r := remote.Inner.(lexicon.SchemaString)
+		// NOTE: default can change safely
+		if !eqOptString(l.Const, r.Const) {
+			issues = append(issues, LintIssue{
+				NSID:            nsid,
+				LintLevel:       "error",
+				LintName:        "const-value",
+				LintDescription: "schema const value change",
+				Message:         fmt.Sprintf("const value changed (%s)", name),
+			})
+		}
+		sort.Strings(l.Enum)
+		sort.Strings(r.Enum)
+		if !reflect.DeepEqual(l.Enum, r.Enum) {
+			issues = append(issues, LintIssue{
+				NSID:            nsid,
+				LintLevel:       "error",
+				LintName:        "enum-values",
+				LintDescription: "schema enum values change",
+				Message:         fmt.Sprintf("string enum value changed (%s)", name),
+			})
+		}
+		// NOTE: known values can change safely
+		if !eqOptInt(l.MinLength, r.MinLength) || !eqOptInt(l.MaxLength, r.MaxLength) || !eqOptInt(l.MinGraphemes, r.MinGraphemes) || !eqOptInt(l.MaxGraphemes, r.MaxGraphemes) {
+			issues = append(issues, LintIssue{
+				NSID:            nsid,
+				LintLevel:       "warn",
+				LintName:        "string-length",
+				LintDescription: "string min/max length change",
+				Message:         fmt.Sprintf("string min/max length change (%s)", name),
+			})
+		}
+		if !eqOptString(l.Format, r.Format) {
+			issues = append(issues, LintIssue{
+				NSID:            nsid,
+				LintLevel:       "error",
+				LintName:        "string-format",
+				LintDescription: "string format change",
+				Message:         fmt.Sprintf("string format changed (%s)", name),
+			})
+		}
+	case lexicon.SchemaBytes:
+		r := remote.Inner.(lexicon.SchemaBytes)
+		if !eqOptInt(l.MinLength, r.MinLength) || !eqOptInt(l.MaxLength, r.MaxLength) {
+			issues = append(issues, LintIssue{
+				NSID:            nsid,
+				LintLevel:       "warn",
+				LintName:        "bytes-length",
+				LintDescription: "bytes min/max length change",
+				Message:         fmt.Sprintf("bytes min/max length change (%s)", name),
+			})
+		}
+	case lexicon.SchemaCIDLink:
+		// pass
+	case lexicon.SchemaArray:
+		r := remote.Inner.(lexicon.SchemaArray)
+		if !eqOptInt(l.MinLength, r.MinLength) || !eqOptInt(l.MaxLength, r.MaxLength) {
+			issues = append(issues, LintIssue{
+				NSID:            nsid,
+				LintLevel:       "warn",
+				LintName:        "array-length",
+				LintDescription: "array min/max length change",
+				Message:         fmt.Sprintf("array min/max length change (%s)", name),
+			})
+		}
+		issues = append(issues, breakingDefs(nsid, name, l.Items, r.Items)...)
+	case lexicon.SchemaObject:
+		r := remote.Inner.(lexicon.SchemaObject)
+		sort.Strings(l.Required)
+		sort.Strings(r.Required)
+		if !reflect.DeepEqual(l.Required, r.Required) {
+			issues = append(issues, LintIssue{
+				NSID:            nsid,
+				LintLevel:       "error",
+				LintName:        "object-required",
+				LintDescription: "change in which fields are required",
+				Message:         fmt.Sprintf("required fields change (%s)", name),
+			})
+		}
+		sort.Strings(l.Nullable)
+		sort.Strings(r.Nullable)
+		if !reflect.DeepEqual(l.Nullable, r.Nullable) {
+			issues = append(issues, LintIssue{
+				NSID:            nsid,
+				LintLevel:       "error",
+				LintName:        "object-nullable",
+				LintDescription: "change in which fields are nullable",
+				Message:         fmt.Sprintf("nullable fields change (%s)", name),
+			})
+		}
+		issues = append(issues, breakingMaps(nsid, l.Properties, r.Properties)...)
+	case lexicon.SchemaBlob:
+		r := remote.Inner.(lexicon.SchemaBlob)
+		sort.Strings(l.Accept)
+		sort.Strings(r.Accept)
+		if !reflect.DeepEqual(l.Accept, r.Accept) {
+			// TODO: how strong of a warning should this be?
+			issues = append(issues, LintIssue{
+				NSID:            nsid,
+				LintLevel:       "warn",
+				LintName:        "blob-accept",
+				LintDescription: "change in blob accept (content-type)",
+				Message:         fmt.Sprintf("blob accept change (%s)", name),
+			})
+		}
+		if !eqOptInt(l.MaxSize, r.MaxSize) {
+			issues = append(issues, LintIssue{
+				NSID:            nsid,
+				LintLevel:       "warn",
+				LintName:        "blob-size",
+				LintDescription: "blob maximum size change",
+				Message:         fmt.Sprintf("blob max size change (%s)", name),
+			})
+		}
+	case lexicon.SchemaParams:
+		r := remote.Inner.(lexicon.SchemaParams)
+		sort.Strings(l.Required)
+		sort.Strings(r.Required)
+		if !reflect.DeepEqual(l.Required, r.Required) {
+			issues = append(issues, LintIssue{
+				NSID:            nsid,
+				LintLevel:       "error",
+				LintName:        "params-required",
+				LintDescription: "change in which fields are required",
+				Message:         fmt.Sprintf("required fields change (%s)", name),
+			})
+		}
+		issues = append(issues, breakingMaps(nsid, l.Properties, r.Properties)...)
+	case lexicon.SchemaRef:
+		r := remote.Inner.(lexicon.SchemaRef)
+		if l.Ref != r.Ref {
+			// NOTE: if the underlying schemas are the same this could be ok in some situations
+			issues = append(issues, LintIssue{
+				NSID:            nsid,
+				LintLevel:       "warn",
+				LintName:        "ref-change",
+				LintDescription: "change in referenced lexicon",
+				Message:         fmt.Sprintf("ref change (%s): %s != %s", name, l.Ref, r.Ref),
+			})
+		}
+	case lexicon.SchemaUnion:
+		r := remote.Inner.(lexicon.SchemaUnion)
+		if !eqOptBool(l.Closed, r.Closed) {
+			issues = append(issues, LintIssue{
+				NSID:            nsid,
+				LintLevel:       "error",
+				LintName:        "union-open-closed",
+				LintDescription: "can't change union between open and closed",
+				Message:         fmt.Sprintf("union open/closed type changed (%s)", name),
+			})
+		}
+		// check for refs changing in closed union
+		if l.Closed != nil && *l.Closed {
+			sort.Strings(l.Refs)
+			sort.Strings(r.Refs)
+			if !reflect.DeepEqual(l.Refs, r.Refs) {
+				issues = append(issues, LintIssue{
+					NSID:            nsid,
+					LintLevel:       "error",
+					LintName:        "union-closed-refs",
+					LintDescription: "closed unions can not have types (refs) change",
+					Message:         fmt.Sprintf("closed union types (refs) changed (%s)", name),
+				})
+			}
+		}
+	case lexicon.SchemaUnknown, lexicon.SchemaPermissionSet, lexicon.SchemaToken:
+		// pass
+	default:
+		slog.Warn("unhandled schema def type in breaking check", "type", reflect.TypeOf(local.Inner))
+	}
+
+	return issues
+}
+
+// helper to check if two optional (pointer) integers are equal/consistent
+func eqOptInt(a, b *int) bool {
+	if a == nil {
+		return b == nil
+	}
+	if b == nil {
+		return false
+	}
+	return *a == *b
+}
+
+func eqOptBool(a, b *bool) bool {
+	if a == nil {
+		return b == nil
+	}
+	if b == nil {
+		return false
+	}
+	return *a == *b
+}
+
+func eqOptString(a, b *string) bool {
+	if a == nil {
+		return b == nil
+	}
+	if b == nil {
+		return false
+	}
+	return *a == *b
+}
diff --git a/lex/lexlint/lint.go b/lex/lexlint/lint.go
new file mode 100644
index 00000000..ad98d53c
--- /dev/null
+++ b/lex/lexlint/lint.go
@@ -0,0 +1,399 @@
+package lexlint
+
+import (
+	"fmt"
+	"log/slog"
+	"slices"
+	"strings"
+
+	"github.com/bluesky-social/indigo/atproto/lexicon"
+	"github.com/bluesky-social/indigo/atproto/syntax"
+)
+
+type LintIssue struct {
+	FilePath        string      `json:"file-path,omitempty"`
+	NSID            syntax.NSID `json:"nsid,omitempty"`
+	LintLevel       string      `json:"lint-level,omitempty"`
+	LintName        string      `json:"lint-name,omitempty"`
+	LintDescription string      `json:"lint-description,omitempty"`
+	Message         string      `json:"message,omitempty"`
+}
+
+func LintSchemaFile(sf *lexicon.SchemaFile) []LintIssue {
+	issues := []LintIssue{}
+
+	nsid, err := syntax.ParseNSID(sf.ID)
+	if err != nil {
+		issues = append(issues, LintIssue{
+			NSID:            syntax.NSID(sf.ID),
+			LintLevel:       "error",
+			LintName:        "invalid-nsid",
+			LintDescription: "schema file declares NSID with invalid syntax",
+			Message:         fmt.Sprintf("NSID string: %s", sf.ID),
+		})
+	}
+	if nsid == "" {
+		nsid = syntax.NSID(sf.ID)
+	}
+	if sf.Lexicon != 1 {
+		issues = append(issues, LintIssue{
+			NSID:            nsid,
+			LintLevel:       "error",
+			LintName:        "lexicon-version",
+			LintDescription: "unsupported Lexicon language version",
+			Message:         fmt.Sprintf("found version: %d", sf.Lexicon),
+		})
+		return issues
+	}
+
+	for defname, def := range sf.Defs {
+		defiss := lintSchemaDef(nsid, defname, def)
+		if len(defiss) > 0 {
+			issues = append(issues, defiss...)
+		}
+	}
+
+	return issues
+}
+
+func lintSchemaDef(nsid syntax.NSID, defname string, def lexicon.SchemaDef) []LintIssue {
+	issues := []LintIssue{}
+
+	// missing description issue, in case it is needed
+	missingDesc := func() LintIssue {
+		return LintIssue{
+			NSID:            nsid,
+			LintLevel:       "warn",
+			LintName:        "missing-primary-description",
+			LintDescription: "primary types (record, query, procedure, subscription, permission-set) should include a description",
+			Message:         "primary type missing a description",
+		}
+	}
+
+	if err := def.CheckSchema(); err != nil {
+		issues = append(issues, LintIssue{
+			NSID:            nsid,
+			LintLevel:       "error",
+			LintName:        "lexicon-schema",
+			LintDescription: "basic structure schema checks (additional errors may be collapsed)",
+			Message:         err.Error(),
+		})
+	}
+
+	if err := CheckSchemaName(defname); err != nil {
+		issues = append(issues, LintIssue{
+			NSID:            nsid,
+			LintLevel:       "warn",
+			LintName:        "def-name-syntax",
+			LintDescription: "definition name does not follow syntax guidance",
+			Message:         fmt.Sprintf("%s: %s", err.Error(), defname),
+		})
+	}
+
+	if nsid.Name() == "defs" && defname == "main" {
+		issues = append(issues, LintIssue{
+			NSID:            nsid,
+			LintLevel:       "warn",
+			LintName:        "defs-main-definition",
+			LintDescription: "defs schemas should not have a 'main'",
+			Message:         "defs schemas should not have a 'main'",
+		})
+	}
+
+	switch def.Inner.(type) {
+	// NOTE: not requiring description on permission-set
+	case lexicon.SchemaRecord, lexicon.SchemaQuery, lexicon.SchemaProcedure, lexicon.SchemaSubscription:
+		if defname != "main" {
+			issues = append(issues, LintIssue{
+				NSID:            nsid,
+				LintLevel:       "error",
+				LintName:        "non-main-primary",
+				LintDescription: "primary types (record, query, procedure, subscription, permission-set) must be 'main' definition",
+				Message:         fmt.Sprintf("primary definition types must be 'main': %s", defname),
+			})
+		}
+	}
+
+	switch v := def.Inner.(type) {
+	case lexicon.SchemaRecord:
+		if v.Description == nil || *v.Description == "" {
+			issues = append(issues, missingDesc())
+		}
+		reciss := lintSchemaRecursive(nsid, lexicon.SchemaDef{Inner: v.Record})
+		if len(reciss) > 0 {
+			issues = append(issues, reciss...)
+		}
+	case lexicon.SchemaQuery:
+		if v.Description == nil || *v.Description == "" {
+			issues = append(issues, missingDesc())
+		}
+		if v.Parameters != nil {
+			reciss := lintSchemaRecursive(nsid, lexicon.SchemaDef{Inner: *v.Parameters})
+			if len(reciss) > 0 {
+				issues = append(issues, reciss...)
+			}
+		}
+		if v.Output == nil {
+			issues = append(issues, LintIssue{
+				NSID:            nsid,
+				LintLevel:       "warn",
+				LintName:        "endpoint-output-undefined",
+				LintDescription: "API endpoints should define an output (even if empty)",
+				Message:         "missing output definition",
+			})
+		} else {
+			reciss := lintSchemaRecursive(nsid, lexicon.SchemaDef{Inner: *v.Output})
+			if len(reciss) > 0 {
+				issues = append(issues, reciss...)
+			}
+		}
+		// TODO: error names
+	case lexicon.SchemaProcedure:
+		if v.Description == nil || *v.Description == "" {
+			issues = append(issues, missingDesc())
+		}
+		if v.Parameters != nil {
+			reciss := lintSchemaRecursive(nsid, lexicon.SchemaDef{Inner: *v.Parameters})
+			if len(reciss) > 0 {
+				issues = append(issues, reciss...)
+			}
+		}
+		if v.Input != nil {
+			reciss := lintSchemaRecursive(nsid, lexicon.SchemaDef{Inner: *v.Input})
+			if len(reciss) > 0 {
+				issues = append(issues, reciss...)
+			}
+		}
+		if v.Output == nil {
+			issues = append(issues, LintIssue{
+				NSID:            nsid,
+				LintLevel:       "warn",
+				LintName:        "endpoint-output-undefined",
+				LintDescription: "API endpoints should define an output (even if empty)",
+				Message:         "missing output definition",
+			})
+		} else {
+			reciss := lintSchemaRecursive(nsid, lexicon.SchemaDef{Inner: *v.Output})
+			if len(reciss) > 0 {
+				issues = append(issues, reciss...)
+			}
+		}
+		// TODO: error names
+	case lexicon.SchemaSubscription:
+		if v.Description == nil || *v.Description == "" {
+			issues = append(issues, missingDesc())
+		}
+		if v.Parameters != nil {
+			reciss := lintSchemaRecursive(nsid, lexicon.SchemaDef{Inner: *v.Parameters})
+			if len(reciss) > 0 {
+				issues = append(issues, reciss...)
+			}
+		}
+		if v.Message != nil {
+			// TODO: v.Message.Schema must only have local references (same file), and should have at least one defined
+			reciss := lintSchemaRecursive(nsid, lexicon.SchemaDef{Inner: v.Message.Schema})
+			if len(reciss) > 0 {
+				issues = append(issues, reciss...)
+			}
+		} else {
+			issues = append(issues, LintIssue{
+				NSID:            nsid,
+				LintLevel:       "warn",
+				LintName:        "subscription-no-messages",
+				LintDescription: "no subscription message types defined",
+				Message:         "no subscription message types defined",
+			})
+		}
+		// TODO: at least one message type
+	case lexicon.SchemaPermissionSet:
+		if v.Title == nil || *v.Title == "" {
+			issues = append(issues, LintIssue{
+				NSID:            nsid,
+				LintLevel:       "warn",
+				LintName:        "permissionset-no-title",
+				LintDescription: "permission sets should include a title",
+				Message:         "missing title",
+			})
+		}
+		// TODO: missing detail
+		// TODO: translated descriptions?
+		if len(v.Permissions) == 0 {
+			issues = append(issues, LintIssue{
+				NSID:            nsid,
+				LintLevel:       "warn",
+				LintName:        "permissionset-no-permissions",
+				LintDescription: "permission sets should define at least one permission",
+				Message:         "empty permission set",
+			})
+		}
+		for _, perm := range v.Permissions {
+			// TODO: any lints on permissions?
+			_ = perm
+		}
+	case lexicon.SchemaPermission, lexicon.SchemaBoolean, lexicon.SchemaInteger, lexicon.SchemaString, lexicon.SchemaBytes, lexicon.SchemaCIDLink, lexicon.SchemaArray, lexicon.SchemaObject, lexicon.SchemaBlob, lexicon.SchemaToken, lexicon.SchemaRef, lexicon.SchemaUnion, lexicon.SchemaUnknown:
+		reciss := lintSchemaRecursive(nsid, def)
+		if len(reciss) > 0 {
+			issues = append(issues, reciss...)
+		}
+	default:
+		slog.Info("no lint rules for top-level schema definition type", "type", fmt.Sprintf("%T", def.Inner))
+	}
+	return issues
+}
+
+func lintSchemaRecursive(nsid syntax.NSID, def lexicon.SchemaDef) []LintIssue {
+	issues := []LintIssue{}
+
+	switch v := def.Inner.(type) {
+	case lexicon.SchemaBoolean:
+		// TODO: default true
+		// TODO: both default and const
+	case lexicon.SchemaInteger:
+		// TODO: both default and const
+	case lexicon.SchemaString:
+		// TODO: format and length limits
+		// TODO: grapheme limit set, and maxlen either too low or not set
+		// TODO: format=handle strings within an record type
+		if v.MaxLength != nil && *v.MaxLength > 20*1024 {
+			issues = append(issues, LintIssue{
+				NSID:            nsid,
+				LintLevel:       "warn",
+				LintName:        "large-string",
+				LintDescription: "string field with large maximum size (use blobs instead)",
+				Message:         "large max length",
+			})
+		}
+		if v.Format == nil && v.MaxLength == nil && v.MaxGraphemes == nil {
+			issues = append(issues, LintIssue{
+				NSID:            nsid,
+				LintLevel:       "warn",
+				LintName:        "unlimited-string",
+				LintDescription: "string field with no format or maximum size",
+				Message:         "no max length",
+			})
+		}
+		for _, val := range v.KnownValues {
+			if strings.HasPrefix(val, "#") {
+				issues = append(issues, LintIssue{
+					NSID:            nsid,
+					LintLevel:       "warn",
+					LintName:        "known-string-local-ref",
+					LintDescription: "string knownValues entry which seems to be a local reference",
+					Message:         fmt.Sprintf("possible local ref: %s", val),
+				})
+			}
+		}
+	case lexicon.SchemaBytes:
+		if v.MaxLength == nil {
+			issues = append(issues, LintIssue{
+				NSID:            nsid,
+				LintLevel:       "warn",
+				LintName:        "unlimited-bytes",
+				LintDescription: "bytes field with no maximum size",
+				Message:         "no max length",
+			})
+		}
+		if v.MaxLength != nil && *v.MaxLength > 20*1024 {
+			// TODO: limit this to 'record' schemas?
+			issues = append(issues, LintIssue{
+				NSID:            nsid,
+				LintLevel:       "warn",
+				LintName:        "large-bytes",
+				LintDescription: "bytes field with large maximum size (use blobs instead)",
+				Message:         "large max length",
+			})
+		}
+	case lexicon.SchemaCIDLink:
+		// pass
+	case lexicon.SchemaBlob:
+		// pass
+	case lexicon.SchemaArray:
+		reciss := lintSchemaRecursive(nsid, v.Items)
+		if len(reciss) > 0 {
+			issues = append(issues, reciss...)
+		}
+	case lexicon.SchemaObject:
+		// NOTE: CheckSchema already verifies that nullable and required are valid against property keys
+		for fieldName, propdef := range v.Properties {
+			reciss := lintSchemaRecursive(nsid, propdef)
+			if len(reciss) > 0 {
+				issues = append(issues, reciss...)
+			}
+			if err := CheckSchemaName(fieldName); err != nil {
+				issues = append(issues, LintIssue{
+					NSID:            nsid,
+					LintLevel:       "warn",
+					LintName:        "field-name-syntax",
+					LintDescription: "field name does not follow syntax guidance",
+					Message:         fmt.Sprintf("%s: %s", err.Error(), fieldName),
+				})
+			}
+		}
+		for _, k := range v.Nullable {
+			if !slices.Contains(v.Required, k) {
+				issues = append(issues, LintIssue{
+					NSID:            nsid,
+					LintLevel:       "warn",
+					LintName:        "nullable-and-optional",
+					LintDescription: "object properties should not be both optional and nullable",
+					Message:         fmt.Sprintf("field is both nullable and optional: %s", k),
+				})
+			}
+		}
+	case lexicon.SchemaParams:
+		// NOTE: CheckSchema already verifies that required are valid against property keys
+		for fieldName, propdef := range v.Properties {
+			reciss := lintSchemaRecursive(nsid, propdef)
+			if len(reciss) > 0 {
+				issues = append(issues, reciss...)
+			}
+			if err := CheckSchemaName(fieldName); err != nil {
+				issues = append(issues, LintIssue{
+					NSID:            nsid,
+					LintLevel:       "warn",
+					LintName:        "field-name-syntax",
+					LintDescription: "field name does not follow syntax guidance",
+					Message:         fmt.Sprintf("%s: %s", err.Error(), fieldName),
+				})
+			}
+		}
+	case lexicon.SchemaToken:
+		if v.Description == nil || *v.Description == "" {
+			issues = append(issues, LintIssue{
+				NSID:            nsid,
+				LintLevel:       "warn",
+				LintName:        "undescribed-token",
+				LintDescription: "token without description field",
+				Message:         "empty description",
+			})
+		}
+	case lexicon.SchemaRef:
+		// TODO: resolve? locally vs globally?
+	case lexicon.SchemaUnion:
+		// TODO: open vs closed?
+		// TODO: check that refs actually resolve?
+	case lexicon.SchemaUnknown:
+		// pass
+	case lexicon.SchemaBody:
+		if v.Schema != nil {
+			// NOTE: CheckSchema already verified that v.Schema is an object, ref, or union
+			reciss := lintSchemaRecursive(nsid, *v.Schema)
+			if len(reciss) > 0 {
+				issues = append(issues, reciss...)
+			}
+		}
+		if v.Encoding == "" {
+			issues = append(issues, LintIssue{
+				NSID:            nsid,
+				LintLevel:       "warn",
+				LintName:        "unspecified-encoding",
+				LintDescription: "body encoding not specified",
+				Message:         "missing encoding",
+			})
+		}
+	default:
+		slog.Info("no lint rules for recursive schema type", "type", fmt.Sprintf("%T", def.Inner), "nsid", nsid)
+	}
+
+	return issues
+}
diff --git a/lex/lexlint/syntax.go b/lex/lexlint/syntax.go
new file mode 100644
index 00000000..ed376851
--- /dev/null
+++ b/lex/lexlint/syntax.go
@@ -0,0 +1,21 @@
+package lexlint
+
+import (
+	"errors"
+	"regexp"
+)
+
+var schemaNameRegex = regexp.MustCompile(`^[a-zA-Z]([a-zA-Z0-9]{0,255})?$`)
+
+func CheckSchemaName(raw string) error {
+	if raw == "" {
+		return errors.New("empty name")
+	}
+	if len(raw) > 255 {
+		return errors.New("name is too long (255 chars max)")
+	}
+	if !schemaNameRegex.MatchString(raw) {
+		return errors.New("name doesn't match recommended syntax/characters")
+	}
+	return nil
+}
diff --git a/lexicons/app/bsky/actor/defs.json b/lexicons/app/bsky/actor/defs.json
new file mode 100644
index 00000000..75297923
--- /dev/null
+++ b/lexicons/app/bsky/actor/defs.json
@@ -0,0 +1,647 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.actor.defs",
+  "defs": {
+    "profileViewBasic": {
+      "type": "object",
+      "required": ["did", "handle"],
+      "properties": {
+        "did": { "type": "string", "format": "did" },
+        "handle": { "type": "string", "format": "handle" },
+        "displayName": {
+          "type": "string",
+          "maxGraphemes": 64,
+          "maxLength": 640
+        },
+        "pronouns": { "type": "string" },
+        "avatar": { "type": "string", "format": "uri" },
+        "associated": {
+          "type": "ref",
+          "ref": "#profileAssociated"
+        },
+        "viewer": { "type": "ref", "ref": "#viewerState" },
+        "labels": {
+          "type": "array",
+          "items": { "type": "ref", "ref": "com.atproto.label.defs#label" }
+        },
+        "createdAt": { "type": "string", "format": "datetime" },
+        "verification": {
+          "type": "ref",
+          "ref": "#verificationState"
+        },
+        "status": {
+          "type": "ref",
+          "ref": "#statusView"
+        },
+        "debug": {
+          "type": "unknown",
+          "description": "Debug information for internal development"
+        }
+      }
+    },
+    "profileView": {
+      "type": "object",
+      "required": ["did", "handle"],
+      "properties": {
+        "did": { "type": "string", "format": "did" },
+        "handle": { "type": "string", "format": "handle" },
+        "displayName": {
+          "type": "string",
+          "maxGraphemes": 64,
+          "maxLength": 640
+        },
+        "pronouns": { "type": "string" },
+        "description": {
+          "type": "string",
+          "maxGraphemes": 256,
+          "maxLength": 2560
+        },
+        "avatar": { "type": "string", "format": "uri" },
+        "associated": {
+          "type": "ref",
+          "ref": "#profileAssociated"
+        },
+        "indexedAt": { "type": "string", "format": "datetime" },
+        "createdAt": { "type": "string", "format": "datetime" },
+        "viewer": { "type": "ref", "ref": "#viewerState" },
+        "labels": {
+          "type": "array",
+          "items": { "type": "ref", "ref": "com.atproto.label.defs#label" }
+        },
+        "verification": {
+          "type": "ref",
+          "ref": "#verificationState"
+        },
+        "status": {
+          "type": "ref",
+          "ref": "#statusView"
+        },
+        "debug": {
+          "type": "unknown",
+          "description": "Debug information for internal development"
+        }
+      }
+    },
+    "profileViewDetailed": {
+      "type": "object",
+      "required": ["did", "handle"],
+      "properties": {
+        "did": { "type": "string", "format": "did" },
+        "handle": { "type": "string", "format": "handle" },
+        "displayName": {
+          "type": "string",
+          "maxGraphemes": 64,
+          "maxLength": 640
+        },
+        "description": {
+          "type": "string",
+          "maxGraphemes": 256,
+          "maxLength": 2560
+        },
+        "pronouns": { "type": "string" },
+        "website": { "type": "string", "format": "uri" },
+        "avatar": { "type": "string", "format": "uri" },
+        "banner": { "type": "string", "format": "uri" },
+        "followersCount": { "type": "integer" },
+        "followsCount": { "type": "integer" },
+        "postsCount": { "type": "integer" },
+        "associated": {
+          "type": "ref",
+          "ref": "#profileAssociated"
+        },
+        "joinedViaStarterPack": {
+          "type": "ref",
+          "ref": "app.bsky.graph.defs#starterPackViewBasic"
+        },
+        "indexedAt": { "type": "string", "format": "datetime" },
+        "createdAt": { "type": "string", "format": "datetime" },
+        "viewer": { "type": "ref", "ref": "#viewerState" },
+        "labels": {
+          "type": "array",
+          "items": { "type": "ref", "ref": "com.atproto.label.defs#label" }
+        },
+        "pinnedPost": {
+          "type": "ref",
+          "ref": "com.atproto.repo.strongRef"
+        },
+        "verification": {
+          "type": "ref",
+          "ref": "#verificationState"
+        },
+        "status": {
+          "type": "ref",
+          "ref": "#statusView"
+        },
+        "debug": {
+          "type": "unknown",
+          "description": "Debug information for internal development"
+        }
+      }
+    },
+    "profileAssociated": {
+      "type": "object",
+      "properties": {
+        "lists": { "type": "integer" },
+        "feedgens": { "type": "integer" },
+        "starterPacks": { "type": "integer" },
+        "labeler": { "type": "boolean" },
+        "chat": { "type": "ref", "ref": "#profileAssociatedChat" },
+        "activitySubscription": {
+          "type": "ref",
+          "ref": "#profileAssociatedActivitySubscription"
+        }
+      }
+    },
+    "profileAssociatedChat": {
+      "type": "object",
+      "required": ["allowIncoming"],
+      "properties": {
+        "allowIncoming": {
+          "type": "string",
+          "knownValues": ["all", "none", "following"]
+        }
+      }
+    },
+    "profileAssociatedActivitySubscription": {
+      "type": "object",
+      "required": ["allowSubscriptions"],
+      "properties": {
+        "allowSubscriptions": {
+          "type": "string",
+          "knownValues": ["followers", "mutuals", "none"]
+        }
+      }
+    },
+    "viewerState": {
+      "type": "object",
+      "description": "Metadata about the requesting account's relationship with the subject account. Only has meaningful content for authed requests.",
+      "properties": {
+        "muted": { "type": "boolean" },
+        "mutedByList": {
+          "type": "ref",
+          "ref": "app.bsky.graph.defs#listViewBasic"
+        },
+        "blockedBy": { "type": "boolean" },
+        "blocking": { "type": "string", "format": "at-uri" },
+        "blockingByList": {
+          "type": "ref",
+          "ref": "app.bsky.graph.defs#listViewBasic"
+        },
+        "following": { "type": "string", "format": "at-uri" },
+        "followedBy": { "type": "string", "format": "at-uri" },
+        "knownFollowers": {
+          "description": "This property is present only in selected cases, as an optimization.",
+          "type": "ref",
+          "ref": "#knownFollowers"
+        },
+        "activitySubscription": {
+          "description": "This property is present only in selected cases, as an optimization.",
+          "type": "ref",
+          "ref": "app.bsky.notification.defs#activitySubscription"
+        }
+      }
+    },
+    "knownFollowers": {
+      "type": "object",
+      "description": "The subject's followers whom you also follow",
+      "required": ["count", "followers"],
+      "properties": {
+        "count": { "type": "integer" },
+        "followers": {
+          "type": "array",
+          "minLength": 0,
+          "maxLength": 5,
+          "items": {
+            "type": "ref",
+            "ref": "#profileViewBasic"
+          }
+        }
+      }
+    },
+    "verificationState": {
+      "type": "object",
+      "description": "Represents the verification information about the user this object is attached to.",
+      "required": ["verifications", "verifiedStatus", "trustedVerifierStatus"],
+      "properties": {
+        "verifications": {
+          "type": "array",
+          "description": "All verifications issued by trusted verifiers on behalf of this user. Verifications by untrusted verifiers are not included.",
+          "items": { "type": "ref", "ref": "#verificationView" }
+        },
+        "verifiedStatus": {
+          "type": "string",
+          "description": "The user's status as a verified account.",
+          "knownValues": ["valid", "invalid", "none"]
+        },
+        "trustedVerifierStatus": {
+          "type": "string",
+          "description": "The user's status as a trusted verifier.",
+          "knownValues": ["valid", "invalid", "none"]
+        }
+      }
+    },
+    "verificationView": {
+      "type": "object",
+      "description": "An individual verification for an associated subject.",
+      "required": ["issuer", "uri", "isValid", "createdAt"],
+      "properties": {
+        "issuer": {
+          "type": "string",
+          "description": "The user who issued this verification.",
+          "format": "did"
+        },
+        "uri": {
+          "type": "string",
+          "description": "The AT-URI of the verification record.",
+          "format": "at-uri"
+        },
+        "isValid": {
+          "type": "boolean",
+          "description": "True if the verification passes validation, otherwise false."
+        },
+        "createdAt": {
+          "type": "string",
+          "description": "Timestamp when the verification was created.",
+          "format": "datetime"
+        }
+      }
+    },
+    "preferences": {
+      "type": "array",
+      "items": {
+        "type": "union",
+        "refs": [
+          "#adultContentPref",
+          "#contentLabelPref",
+          "#savedFeedsPref",
+          "#savedFeedsPrefV2",
+          "#personalDetailsPref",
+          "#feedViewPref",
+          "#threadViewPref",
+          "#interestsPref",
+          "#mutedWordsPref",
+          "#hiddenPostsPref",
+          "#bskyAppStatePref",
+          "#labelersPref",
+          "#postInteractionSettingsPref",
+          "#verificationPrefs"
+        ]
+      }
+    },
+    "adultContentPref": {
+      "type": "object",
+      "required": ["enabled"],
+      "properties": {
+        "enabled": { "type": "boolean", "default": false }
+      }
+    },
+    "contentLabelPref": {
+      "type": "object",
+      "required": ["label", "visibility"],
+      "properties": {
+        "labelerDid": {
+          "type": "string",
+          "description": "Which labeler does this preference apply to? If undefined, applies globally.",
+          "format": "did"
+        },
+        "label": { "type": "string" },
+        "visibility": {
+          "type": "string",
+          "knownValues": ["ignore", "show", "warn", "hide"]
+        }
+      }
+    },
+    "savedFeed": {
+      "type": "object",
+      "required": ["id", "type", "value", "pinned"],
+      "properties": {
+        "id": {
+          "type": "string"
+        },
+        "type": {
+          "type": "string",
+          "knownValues": ["feed", "list", "timeline"]
+        },
+        "value": {
+          "type": "string"
+        },
+        "pinned": {
+          "type": "boolean"
+        }
+      }
+    },
+    "savedFeedsPrefV2": {
+      "type": "object",
+      "required": ["items"],
+      "properties": {
+        "items": {
+          "type": "array",
+          "items": {
+            "type": "ref",
+            "ref": "app.bsky.actor.defs#savedFeed"
+          }
+        }
+      }
+    },
+    "savedFeedsPref": {
+      "type": "object",
+      "required": ["pinned", "saved"],
+      "properties": {
+        "pinned": {
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "at-uri"
+          }
+        },
+        "saved": {
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "at-uri"
+          }
+        },
+        "timelineIndex": {
+          "type": "integer"
+        }
+      }
+    },
+    "personalDetailsPref": {
+      "type": "object",
+      "properties": {
+        "birthDate": {
+          "type": "string",
+          "format": "datetime",
+          "description": "The birth date of account owner."
+        }
+      }
+    },
+    "feedViewPref": {
+      "type": "object",
+      "required": ["feed"],
+      "properties": {
+        "feed": {
+          "type": "string",
+          "description": "The URI of the feed, or an identifier which describes the feed."
+        },
+        "hideReplies": {
+          "type": "boolean",
+          "description": "Hide replies in the feed."
+        },
+        "hideRepliesByUnfollowed": {
+          "type": "boolean",
+          "description": "Hide replies in the feed if they are not by followed users.",
+          "default": true
+        },
+        "hideRepliesByLikeCount": {
+          "type": "integer",
+          "description": "Hide replies in the feed if they do not have this number of likes."
+        },
+        "hideReposts": {
+          "type": "boolean",
+          "description": "Hide reposts in the feed."
+        },
+        "hideQuotePosts": {
+          "type": "boolean",
+          "description": "Hide quote posts in the feed."
+        }
+      }
+    },
+    "threadViewPref": {
+      "type": "object",
+      "properties": {
+        "sort": {
+          "type": "string",
+          "description": "Sorting mode for threads.",
+          "knownValues": ["oldest", "newest", "most-likes", "random", "hotness"]
+        }
+      }
+    },
+    "interestsPref": {
+      "type": "object",
+      "required": ["tags"],
+      "properties": {
+        "tags": {
+          "type": "array",
+          "maxLength": 100,
+          "items": { "type": "string", "maxLength": 640, "maxGraphemes": 64 },
+          "description": "A list of tags which describe the account owner's interests gathered during onboarding."
+        }
+      }
+    },
+    "mutedWordTarget": {
+      "type": "string",
+      "knownValues": ["content", "tag"],
+      "maxLength": 640,
+      "maxGraphemes": 64
+    },
+    "mutedWord": {
+      "type": "object",
+      "description": "A word that the account owner has muted.",
+      "required": ["value", "targets"],
+      "properties": {
+        "id": { "type": "string" },
+        "value": {
+          "type": "string",
+          "description": "The muted word itself.",
+          "maxLength": 10000,
+          "maxGraphemes": 1000
+        },
+        "targets": {
+          "type": "array",
+          "description": "The intended targets of the muted word.",
+          "items": {
+            "type": "ref",
+            "ref": "app.bsky.actor.defs#mutedWordTarget"
+          }
+        },
+        "actorTarget": {
+          "type": "string",
+          "description": "Groups of users to apply the muted word to. If undefined, applies to all users.",
+          "knownValues": ["all", "exclude-following"],
+          "default": "all"
+        },
+        "expiresAt": {
+          "type": "string",
+          "format": "datetime",
+          "description": "The date and time at which the muted word will expire and no longer be applied."
+        }
+      }
+    },
+    "mutedWordsPref": {
+      "type": "object",
+      "required": ["items"],
+      "properties": {
+        "items": {
+          "type": "array",
+          "items": {
+            "type": "ref",
+            "ref": "app.bsky.actor.defs#mutedWord"
+          },
+          "description": "A list of words the account owner has muted."
+        }
+      }
+    },
+    "hiddenPostsPref": {
+      "type": "object",
+      "required": ["items"],
+      "properties": {
+        "items": {
+          "type": "array",
+          "items": { "type": "string", "format": "at-uri" },
+          "description": "A list of URIs of posts the account owner has hidden."
+        }
+      }
+    },
+    "labelersPref": {
+      "type": "object",
+      "required": ["labelers"],
+      "properties": {
+        "labelers": {
+          "type": "array",
+          "items": {
+            "type": "ref",
+            "ref": "#labelerPrefItem"
+          }
+        }
+      }
+    },
+    "labelerPrefItem": {
+      "type": "object",
+      "required": ["did"],
+      "properties": {
+        "did": {
+          "type": "string",
+          "format": "did"
+        }
+      }
+    },
+    "bskyAppStatePref": {
+      "description": "A grab bag of state that's specific to the bsky.app program. Third-party apps shouldn't use this.",
+      "type": "object",
+      "properties": {
+        "activeProgressGuide": {
+          "type": "ref",
+          "ref": "#bskyAppProgressGuide"
+        },
+        "queuedNudges": {
+          "description": "An array of tokens which identify nudges (modals, popups, tours, highlight dots) that should be shown to the user.",
+          "type": "array",
+          "maxLength": 1000,
+          "items": { "type": "string", "maxLength": 100 }
+        },
+        "nuxs": {
+          "description": "Storage for NUXs the user has encountered.",
+          "type": "array",
+          "maxLength": 100,
+          "items": {
+            "type": "ref",
+            "ref": "app.bsky.actor.defs#nux"
+          }
+        }
+      }
+    },
+    "bskyAppProgressGuide": {
+      "description": "If set, an active progress guide. Once completed, can be set to undefined. Should have unspecced fields tracking progress.",
+      "type": "object",
+      "required": ["guide"],
+      "properties": {
+        "guide": { "type": "string", "maxLength": 100 }
+      }
+    },
+    "nux": {
+      "type": "object",
+      "description": "A new user experiences (NUX) storage object",
+      "required": ["id", "completed"],
+      "properties": {
+        "id": {
+          "type": "string",
+          "maxLength": 100
+        },
+        "completed": {
+          "type": "boolean",
+          "default": false
+        },
+        "data": {
+          "description": "Arbitrary data for the NUX. The structure is defined by the NUX itself. Limited to 300 characters.",
+          "type": "string",
+          "maxLength": 3000,
+          "maxGraphemes": 300
+        },
+        "expiresAt": {
+          "type": "string",
+          "format": "datetime",
+          "description": "The date and time at which the NUX will expire and should be considered completed."
+        }
+      }
+    },
+    "verificationPrefs": {
+      "type": "object",
+      "description": "Preferences for how verified accounts appear in the app.",
+      "required": [],
+      "properties": {
+        "hideBadges": {
+          "description": "Hide the blue check badges for verified accounts and trusted verifiers.",
+          "type": "boolean",
+          "default": false
+        }
+      }
+    },
+    "postInteractionSettingsPref": {
+      "type": "object",
+      "description": "Default post interaction settings for the account. These values should be applied as default values when creating new posts. These refs should mirror the threadgate and postgate records exactly.",
+      "required": [],
+      "properties": {
+        "threadgateAllowRules": {
+          "description": "Matches threadgate record. List of rules defining who can reply to this users posts. If value is an empty array, no one can reply. If value is undefined, anyone can reply.",
+          "type": "array",
+          "maxLength": 5,
+          "items": {
+            "type": "union",
+            "refs": [
+              "app.bsky.feed.threadgate#mentionRule",
+              "app.bsky.feed.threadgate#followerRule",
+              "app.bsky.feed.threadgate#followingRule",
+              "app.bsky.feed.threadgate#listRule"
+            ]
+          }
+        },
+        "postgateEmbeddingRules": {
+          "description": "Matches postgate record. List of rules defining who can embed this users posts. If value is an empty array or is undefined, no particular rules apply and anyone can embed.",
+          "type": "array",
+          "maxLength": 5,
+          "items": {
+            "type": "union",
+            "refs": ["app.bsky.feed.postgate#disableRule"]
+          }
+        }
+      }
+    },
+    "statusView": {
+      "type": "object",
+      "required": ["status", "record"],
+      "properties": {
+        "status": {
+          "type": "string",
+          "description": "The status for the account.",
+          "knownValues": ["app.bsky.actor.status#live"]
+        },
+        "record": { "type": "unknown" },
+        "embed": {
+          "type": "union",
+          "description": "An optional embed associated with the status.",
+          "refs": ["app.bsky.embed.external#view"]
+        },
+        "expiresAt": {
+          "type": "string",
+          "description": "The date when this status will expire. The application might choose to no longer return the status after expiration.",
+          "format": "datetime"
+        },
+        "isActive": {
+          "type": "boolean",
+          "description": "True if the status is not expired, false if it is expired. Only present if expiration was set."
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/actor/getPreferences.json b/lexicons/app/bsky/actor/getPreferences.json
new file mode 100644
index 00000000..e6356a86
--- /dev/null
+++ b/lexicons/app/bsky/actor/getPreferences.json
@@ -0,0 +1,27 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.actor.getPreferences",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get private preferences attached to the current account. Expected use is synchronization between multiple devices, and import/export during account migration. Requires auth.",
+      "parameters": {
+        "type": "params",
+        "properties": {}
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["preferences"],
+          "properties": {
+            "preferences": {
+              "type": "ref",
+              "ref": "app.bsky.actor.defs#preferences"
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/actor/getProfile.json b/lexicons/app/bsky/actor/getProfile.json
new file mode 100644
index 00000000..15b0fcc2
--- /dev/null
+++ b/lexicons/app/bsky/actor/getProfile.json
@@ -0,0 +1,28 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.actor.getProfile",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get detailed profile view of an actor. Does not require auth, but contains relevant metadata with auth.",
+      "parameters": {
+        "type": "params",
+        "required": ["actor"],
+        "properties": {
+          "actor": {
+            "type": "string",
+            "format": "at-identifier",
+            "description": "Handle or DID of account to fetch profile of."
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "ref",
+          "ref": "app.bsky.actor.defs#profileViewDetailed"
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/actor/getProfiles.json b/lexicons/app/bsky/actor/getProfiles.json
new file mode 100644
index 00000000..c89622cb
--- /dev/null
+++ b/lexicons/app/bsky/actor/getProfiles.json
@@ -0,0 +1,37 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.actor.getProfiles",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get detailed profile views of multiple actors.",
+      "parameters": {
+        "type": "params",
+        "required": ["actors"],
+        "properties": {
+          "actors": {
+            "type": "array",
+            "items": { "type": "string", "format": "at-identifier" },
+            "maxLength": 25
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["profiles"],
+          "properties": {
+            "profiles": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "app.bsky.actor.defs#profileViewDetailed"
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/actor/getSuggestions.json b/lexicons/app/bsky/actor/getSuggestions.json
new file mode 100644
index 00000000..31e52652
--- /dev/null
+++ b/lexicons/app/bsky/actor/getSuggestions.json
@@ -0,0 +1,43 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.actor.getSuggestions",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get a list of suggested actors. Expected use is discovery of accounts to follow during new account onboarding.",
+      "parameters": {
+        "type": "params",
+        "properties": {
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 100,
+            "default": 50
+          },
+          "cursor": { "type": "string" }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["actors"],
+          "properties": {
+            "cursor": { "type": "string" },
+            "actors": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "app.bsky.actor.defs#profileView"
+              }
+            },
+            "recId": {
+              "type": "integer",
+              "description": "Snowflake for this recommendation, use when submitting recommendation events."
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/actor/profile.json b/lexicons/app/bsky/actor/profile.json
new file mode 100644
index 00000000..dffd48b2
--- /dev/null
+++ b/lexicons/app/bsky/actor/profile.json
@@ -0,0 +1,60 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.actor.profile",
+  "defs": {
+    "main": {
+      "type": "record",
+      "description": "A declaration of a Bluesky account profile.",
+      "key": "literal:self",
+      "record": {
+        "type": "object",
+        "properties": {
+          "displayName": {
+            "type": "string",
+            "maxGraphemes": 64,
+            "maxLength": 640
+          },
+          "description": {
+            "type": "string",
+            "description": "Free-form profile description text.",
+            "maxGraphemes": 256,
+            "maxLength": 2560
+          },
+          "pronouns": {
+            "type": "string",
+            "description": "Free-form pronouns text.",
+            "maxGraphemes": 20,
+            "maxLength": 200
+          },
+          "website": { "type": "string", "format": "uri" },
+          "avatar": {
+            "type": "blob",
+            "description": "Small image to be displayed next to posts from account. AKA, 'profile picture'",
+            "accept": ["image/png", "image/jpeg"],
+            "maxSize": 1000000
+          },
+          "banner": {
+            "type": "blob",
+            "description": "Larger horizontal image to display behind profile view.",
+            "accept": ["image/png", "image/jpeg"],
+            "maxSize": 1000000
+          },
+          "labels": {
+            "type": "union",
+            "description": "Self-label values, specific to the Bluesky application, on the overall account.",
+            "refs": ["com.atproto.label.defs#selfLabels"]
+          },
+          "joinedViaStarterPack": {
+            "type": "ref",
+            "ref": "com.atproto.repo.strongRef"
+          },
+          "pinnedPost": {
+            "type": "ref",
+            "ref": "com.atproto.repo.strongRef"
+          },
+          "createdAt": { "type": "string", "format": "datetime" }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/actor/putPreferences.json b/lexicons/app/bsky/actor/putPreferences.json
new file mode 100644
index 00000000..425ea28c
--- /dev/null
+++ b/lexicons/app/bsky/actor/putPreferences.json
@@ -0,0 +1,23 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.actor.putPreferences",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Set the private preferences attached to the account.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["preferences"],
+          "properties": {
+            "preferences": {
+              "type": "ref",
+              "ref": "app.bsky.actor.defs#preferences"
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/actor/searchActors.json b/lexicons/app/bsky/actor/searchActors.json
new file mode 100644
index 00000000..15ccb082
--- /dev/null
+++ b/lexicons/app/bsky/actor/searchActors.json
@@ -0,0 +1,47 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.actor.searchActors",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Find actors (profiles) matching search criteria. Does not require auth.",
+      "parameters": {
+        "type": "params",
+        "properties": {
+          "term": {
+            "type": "string",
+            "description": "DEPRECATED: use 'q' instead."
+          },
+          "q": {
+            "type": "string",
+            "description": "Search query string. Syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended."
+          },
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 100,
+            "default": 25
+          },
+          "cursor": { "type": "string" }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["actors"],
+          "properties": {
+            "cursor": { "type": "string" },
+            "actors": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "app.bsky.actor.defs#profileView"
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/actor/searchActorsTypeahead.json b/lexicons/app/bsky/actor/searchActorsTypeahead.json
new file mode 100644
index 00000000..4e3cb1b4
--- /dev/null
+++ b/lexicons/app/bsky/actor/searchActorsTypeahead.json
@@ -0,0 +1,45 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.actor.searchActorsTypeahead",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Find actor suggestions for a prefix search term. Expected use is for auto-completion during text field entry. Does not require auth.",
+      "parameters": {
+        "type": "params",
+        "properties": {
+          "term": {
+            "type": "string",
+            "description": "DEPRECATED: use 'q' instead."
+          },
+          "q": {
+            "type": "string",
+            "description": "Search query prefix; not a full query string."
+          },
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 100,
+            "default": 10
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["actors"],
+          "properties": {
+            "actors": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "app.bsky.actor.defs#profileViewBasic"
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/actor/status.json b/lexicons/app/bsky/actor/status.json
new file mode 100644
index 00000000..1fb76ade
--- /dev/null
+++ b/lexicons/app/bsky/actor/status.json
@@ -0,0 +1,37 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.actor.status",
+  "defs": {
+    "main": {
+      "type": "record",
+      "description": "A declaration of a Bluesky account status.",
+      "key": "literal:self",
+      "record": {
+        "type": "object",
+        "required": ["status", "createdAt"],
+        "properties": {
+          "status": {
+            "type": "string",
+            "description": "The status for the account.",
+            "knownValues": ["app.bsky.actor.status#live"]
+          },
+          "embed": {
+            "type": "union",
+            "description": "An optional embed associated with the status.",
+            "refs": ["app.bsky.embed.external"]
+          },
+          "durationMinutes": {
+            "type": "integer",
+            "description": "The duration of the status in minutes. Applications can choose to impose minimum and maximum limits.",
+            "minimum": 1
+          },
+          "createdAt": { "type": "string", "format": "datetime" }
+        }
+      }
+    },
+    "live": {
+      "type": "token",
+      "description": "Advertises an account as currently offering live content."
+    }
+  }
+}
diff --git a/lexicons/app/bsky/ageassurance/begin.json b/lexicons/app/bsky/ageassurance/begin.json
new file mode 100644
index 00000000..7c377449
--- /dev/null
+++ b/lexicons/app/bsky/ageassurance/begin.json
@@ -0,0 +1,48 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.ageassurance.begin",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Initiate Age Assurance for an account.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["email", "language", "countryCode"],
+          "properties": {
+            "email": {
+              "type": "string",
+              "description": "The user's email address to receive Age Assurance instructions."
+            },
+            "language": {
+              "type": "string",
+              "description": "The user's preferred language for communication during the Age Assurance process."
+            },
+            "countryCode": {
+              "type": "string",
+              "description": "An ISO 3166-1 alpha-2 code of the user's location."
+            },
+            "regionCode": {
+              "type": "string",
+              "description": "An optional ISO 3166-2 code of the user's region or state within the country."
+            }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "ref",
+          "ref": "app.bsky.ageassurance.defs#state"
+        }
+      },
+      "errors": [
+        { "name": "InvalidEmail" },
+        { "name": "DidTooLong" },
+        { "name": "InvalidInitiation" },
+        { "name": "RegionNotSupported" }
+      ]
+    }
+  }
+}
diff --git a/lexicons/app/bsky/ageassurance/defs.json b/lexicons/app/bsky/ageassurance/defs.json
new file mode 100644
index 00000000..65f61509
--- /dev/null
+++ b/lexicons/app/bsky/ageassurance/defs.json
@@ -0,0 +1,251 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.ageassurance.defs",
+  "defs": {
+    "access": {
+      "description": "The access level granted based on Age Assurance data we've processed.",
+      "type": "string",
+      "knownValues": ["unknown", "none", "safe", "full"]
+    },
+    "status": {
+      "type": "string",
+      "description": "The status of the Age Assurance process.",
+      "knownValues": ["unknown", "pending", "assured", "blocked"]
+    },
+    "state": {
+      "type": "object",
+      "description": "The user's computed Age Assurance state.",
+      "required": ["status", "access"],
+      "properties": {
+        "lastInitiatedAt": {
+          "type": "string",
+          "format": "datetime",
+          "description": "The timestamp when this state was last updated."
+        },
+        "status": {
+          "type": "ref",
+          "ref": "app.bsky.ageassurance.defs#status"
+        },
+        "access": {
+          "type": "ref",
+          "ref": "app.bsky.ageassurance.defs#access"
+        }
+      }
+    },
+    "stateMetadata": {
+      "type": "object",
+      "description": "Additional metadata needed to compute Age Assurance state client-side.",
+      "required": [],
+      "properties": {
+        "accountCreatedAt": {
+          "type": "string",
+          "format": "datetime",
+          "description": "The account creation timestamp."
+        }
+      }
+    },
+    "config": {
+      "type": "object",
+      "description": "",
+      "required": ["regions"],
+      "properties": {
+        "regions": {
+          "type": "array",
+          "description": "The per-region Age Assurance configuration.",
+          "items": {
+            "type": "ref",
+            "ref": "app.bsky.ageassurance.defs#configRegion"
+          }
+        }
+      }
+    },
+    "configRegion": {
+      "type": "object",
+      "description": "The Age Assurance configuration for a specific region.",
+      "required": ["countryCode", "rules"],
+      "properties": {
+        "countryCode": {
+          "type": "string",
+          "description": "The ISO 3166-1 alpha-2 country code this configuration applies to."
+        },
+        "regionCode": {
+          "type": "string",
+          "description": "The ISO 3166-2 region code this configuration applies to. If omitted, the configuration applies to the entire country."
+        },
+        "rules": {
+          "type": "array",
+          "description": "The ordered list of Age Assurance rules that apply to this region. Rules should be applied in order, and the first matching rule determines the access level granted. The rules array should always include a default rule as the last item.",
+          "items": {
+            "type": "union",
+            "refs": [
+              "#configRegionRuleDefault",
+              "#configRegionRuleIfDeclaredOverAge",
+              "#configRegionRuleIfDeclaredUnderAge",
+              "#configRegionRuleIfAssuredOverAge",
+              "#configRegionRuleIfAssuredUnderAge",
+              "#configRegionRuleIfAccountNewerThan",
+              "#configRegionRuleIfAccountOlderThan"
+            ]
+          }
+        }
+      }
+    },
+    "configRegionRuleDefault": {
+      "type": "object",
+      "description": "Age Assurance rule that applies by default.",
+      "required": ["access"],
+      "properties": {
+        "access": {
+          "type": "ref",
+          "ref": "app.bsky.ageassurance.defs#access"
+        }
+      }
+    },
+    "configRegionRuleIfDeclaredOverAge": {
+      "type": "object",
+      "description": "Age Assurance rule that applies if the user has declared themselves equal-to or over a certain age.",
+      "required": ["age", "access"],
+      "properties": {
+        "age": {
+          "type": "integer",
+          "description": "The age threshold as a whole integer."
+        },
+        "access": {
+          "type": "ref",
+          "ref": "app.bsky.ageassurance.defs#access"
+        }
+      }
+    },
+    "configRegionRuleIfDeclaredUnderAge": {
+      "type": "object",
+      "description": "Age Assurance rule that applies if the user has declared themselves under a certain age.",
+      "required": ["age", "access"],
+      "properties": {
+        "age": {
+          "type": "integer",
+          "description": "The age threshold as a whole integer."
+        },
+        "access": {
+          "type": "ref",
+          "ref": "app.bsky.ageassurance.defs#access"
+        }
+      }
+    },
+    "configRegionRuleIfAssuredOverAge": {
+      "type": "object",
+      "description": "Age Assurance rule that applies if the user has been assured to be equal-to or over a certain age.",
+      "required": ["age", "access"],
+      "properties": {
+        "age": {
+          "type": "integer",
+          "description": "The age threshold as a whole integer."
+        },
+        "access": {
+          "type": "ref",
+          "ref": "app.bsky.ageassurance.defs#access"
+        }
+      }
+    },
+    "configRegionRuleIfAssuredUnderAge": {
+      "type": "object",
+      "description": "Age Assurance rule that applies if the user has been assured to be under a certain age.",
+      "required": ["age", "access"],
+      "properties": {
+        "age": {
+          "type": "integer",
+          "description": "The age threshold as a whole integer."
+        },
+        "access": {
+          "type": "ref",
+          "ref": "app.bsky.ageassurance.defs#access"
+        }
+      }
+    },
+    "configRegionRuleIfAccountNewerThan": {
+      "type": "object",
+      "description": "Age Assurance rule that applies if the account is equal-to or newer than a certain date.",
+      "required": ["date", "access"],
+      "properties": {
+        "date": {
+          "type": "string",
+          "format": "datetime",
+          "description": "The date threshold as a datetime string."
+        },
+        "access": {
+          "type": "ref",
+          "ref": "app.bsky.ageassurance.defs#access"
+        }
+      }
+    },
+    "configRegionRuleIfAccountOlderThan": {
+      "type": "object",
+      "description": "Age Assurance rule that applies if the account is older than a certain date.",
+      "required": ["date", "access"],
+      "properties": {
+        "date": {
+          "type": "string",
+          "format": "datetime",
+          "description": "The date threshold as a datetime string."
+        },
+        "access": {
+          "type": "ref",
+          "ref": "app.bsky.ageassurance.defs#access"
+        }
+      }
+    },
+    "event": {
+      "type": "object",
+      "description": "Object used to store Age Assurance data in stash.",
+      "required": ["createdAt", "status", "access", "attemptId", "countryCode"],
+      "properties": {
+        "createdAt": {
+          "type": "string",
+          "format": "datetime",
+          "description": "The date and time of this write operation."
+        },
+        "attemptId": {
+          "type": "string",
+          "description": "The unique identifier for this instance of the Age Assurance flow, in UUID format."
+        },
+        "status": {
+          "type": "string",
+          "description": "The status of the Age Assurance process.",
+          "knownValues": ["unknown", "pending", "assured", "blocked"]
+        },
+        "access": {
+          "description": "The access level granted based on Age Assurance data we've processed.",
+          "type": "string",
+          "knownValues": ["unknown", "none", "safe", "full"]
+        },
+        "countryCode": {
+          "type": "string",
+          "description": "The ISO 3166-1 alpha-2 country code provided when beginning the Age Assurance flow."
+        },
+        "regionCode": {
+          "type": "string",
+          "description": "The ISO 3166-2 region code provided when beginning the Age Assurance flow."
+        },
+        "email": {
+          "type": "string",
+          "description": "The email used for Age Assurance."
+        },
+        "initIp": {
+          "type": "string",
+          "description": "The IP address used when initiating the Age Assurance flow."
+        },
+        "initUa": {
+          "type": "string",
+          "description": "The user agent used when initiating the Age Assurance flow."
+        },
+        "completeIp": {
+          "type": "string",
+          "description": "The IP address used when completing the Age Assurance flow."
+        },
+        "completeUa": {
+          "type": "string",
+          "description": "The user agent used when completing the Age Assurance flow."
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/ageassurance/getConfig.json b/lexicons/app/bsky/ageassurance/getConfig.json
new file mode 100644
index 00000000..ca27af8d
--- /dev/null
+++ b/lexicons/app/bsky/ageassurance/getConfig.json
@@ -0,0 +1,17 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.ageassurance.getConfig",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Returns Age Assurance configuration for use on the client.",
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "ref",
+          "ref": "app.bsky.ageassurance.defs#config"
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/ageassurance/getState.json b/lexicons/app/bsky/ageassurance/getState.json
new file mode 100644
index 00000000..a334969e
--- /dev/null
+++ b/lexicons/app/bsky/ageassurance/getState.json
@@ -0,0 +1,35 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.ageassurance.getState",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Returns server-computed Age Assurance state, if available, and any additional metadata needed to compute Age Assurance state client-side.",
+      "parameters": {
+        "type": "params",
+        "required": ["countryCode"],
+        "properties": {
+          "countryCode": { "type": "string" },
+          "regionCode": { "type": "string" }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["state", "metadata"],
+          "properties": {
+            "state": {
+              "type": "ref",
+              "ref": "app.bsky.ageassurance.defs#state"
+            },
+            "metadata": {
+              "type": "ref",
+              "ref": "app.bsky.ageassurance.defs#stateMetadata"
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/bookmark/createBookmark.json b/lexicons/app/bsky/bookmark/createBookmark.json
new file mode 100644
index 00000000..d8127e68
--- /dev/null
+++ b/lexicons/app/bsky/bookmark/createBookmark.json
@@ -0,0 +1,27 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.bookmark.createBookmark",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Creates a private bookmark for the specified record. Currently, only `app.bsky.feed.post` records are supported. Requires authentication.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["uri", "cid"],
+          "properties": {
+            "uri": { "type": "string", "format": "at-uri" },
+            "cid": { "type": "string", "format": "cid" }
+          }
+        }
+      },
+      "errors": [
+        {
+          "name": "UnsupportedCollection",
+          "description": "The URI to be bookmarked is for an unsupported collection."
+        }
+      ]
+    }
+  }
+}
diff --git a/lexicons/app/bsky/bookmark/defs.json b/lexicons/app/bsky/bookmark/defs.json
new file mode 100644
index 00000000..b084bfff
--- /dev/null
+++ b/lexicons/app/bsky/bookmark/defs.json
@@ -0,0 +1,38 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.bookmark.defs",
+  "defs": {
+    "bookmark": {
+      "description": "Object used to store bookmark data in stash.",
+      "type": "object",
+      "required": ["subject"],
+      "properties": {
+        "subject": {
+          "description": "A strong ref to the record to be bookmarked. Currently, only `app.bsky.feed.post` records are supported.",
+          "type": "ref",
+          "ref": "com.atproto.repo.strongRef"
+        }
+      }
+    },
+    "bookmarkView": {
+      "type": "object",
+      "required": ["subject", "item"],
+      "properties": {
+        "subject": {
+          "description": "A strong ref to the bookmarked record.",
+          "type": "ref",
+          "ref": "com.atproto.repo.strongRef"
+        },
+        "createdAt": { "type": "string", "format": "datetime" },
+        "item": {
+          "type": "union",
+          "refs": [
+            "app.bsky.feed.defs#blockedPost",
+            "app.bsky.feed.defs#notFoundPost",
+            "app.bsky.feed.defs#postView"
+          ]
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/bookmark/deleteBookmark.json b/lexicons/app/bsky/bookmark/deleteBookmark.json
new file mode 100644
index 00000000..9a615439
--- /dev/null
+++ b/lexicons/app/bsky/bookmark/deleteBookmark.json
@@ -0,0 +1,26 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.bookmark.deleteBookmark",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Deletes a private bookmark for the specified record. Currently, only `app.bsky.feed.post` records are supported. Requires authentication.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["uri"],
+          "properties": {
+            "uri": { "type": "string", "format": "at-uri" }
+          }
+        }
+      },
+      "errors": [
+        {
+          "name": "UnsupportedCollection",
+          "description": "The URI to be bookmarked is for an unsupported collection."
+        }
+      ]
+    }
+  }
+}
diff --git a/lexicons/app/bsky/bookmark/getBookmarks.json b/lexicons/app/bsky/bookmark/getBookmarks.json
new file mode 100644
index 00000000..afda97a4
--- /dev/null
+++ b/lexicons/app/bsky/bookmark/getBookmarks.json
@@ -0,0 +1,39 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.bookmark.getBookmarks",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Gets views of records bookmarked by the authenticated user. Requires authentication.",
+      "parameters": {
+        "type": "params",
+        "properties": {
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 100,
+            "default": 50
+          },
+          "cursor": { "type": "string" }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["bookmarks"],
+          "properties": {
+            "cursor": { "type": "string" },
+            "bookmarks": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "app.bsky.bookmark.defs#bookmarkView"
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/embed/defs.json b/lexicons/app/bsky/embed/defs.json
new file mode 100644
index 00000000..57ffc03a
--- /dev/null
+++ b/lexicons/app/bsky/embed/defs.json
@@ -0,0 +1,15 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.embed.defs",
+  "defs": {
+    "aspectRatio": {
+      "type": "object",
+      "description": "width:height represents an aspect ratio. It may be approximate, and may not correspond to absolute dimensions in any given unit.",
+      "required": ["width", "height"],
+      "properties": {
+        "width": { "type": "integer", "minimum": 1 },
+        "height": { "type": "integer", "minimum": 1 }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/embed/external.json b/lexicons/app/bsky/embed/external.json
new file mode 100644
index 00000000..b9930dff
--- /dev/null
+++ b/lexicons/app/bsky/embed/external.json
@@ -0,0 +1,51 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.embed.external",
+  "defs": {
+    "main": {
+      "type": "object",
+      "description": "A representation of some externally linked content (eg, a URL and 'card'), embedded in a Bluesky record (eg, a post).",
+      "required": ["external"],
+      "properties": {
+        "external": {
+          "type": "ref",
+          "ref": "#external"
+        }
+      }
+    },
+    "external": {
+      "type": "object",
+      "required": ["uri", "title", "description"],
+      "properties": {
+        "uri": { "type": "string", "format": "uri" },
+        "title": { "type": "string" },
+        "description": { "type": "string" },
+        "thumb": {
+          "type": "blob",
+          "accept": ["image/*"],
+          "maxSize": 1000000
+        }
+      }
+    },
+    "view": {
+      "type": "object",
+      "required": ["external"],
+      "properties": {
+        "external": {
+          "type": "ref",
+          "ref": "#viewExternal"
+        }
+      }
+    },
+    "viewExternal": {
+      "type": "object",
+      "required": ["uri", "title", "description"],
+      "properties": {
+        "uri": { "type": "string", "format": "uri" },
+        "title": { "type": "string" },
+        "description": { "type": "string" },
+        "thumb": { "type": "string", "format": "uri" }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/embed/images.json b/lexicons/app/bsky/embed/images.json
new file mode 100644
index 00000000..c6b6fa20
--- /dev/null
+++ b/lexicons/app/bsky/embed/images.json
@@ -0,0 +1,72 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.embed.images",
+  "description": "A set of images embedded in a Bluesky record (eg, a post).",
+  "defs": {
+    "main": {
+      "type": "object",
+      "required": ["images"],
+      "properties": {
+        "images": {
+          "type": "array",
+          "items": { "type": "ref", "ref": "#image" },
+          "maxLength": 4
+        }
+      }
+    },
+    "image": {
+      "type": "object",
+      "required": ["image", "alt"],
+      "properties": {
+        "image": {
+          "type": "blob",
+          "accept": ["image/*"],
+          "maxSize": 1000000
+        },
+        "alt": {
+          "type": "string",
+          "description": "Alt text description of the image, for accessibility."
+        },
+        "aspectRatio": {
+          "type": "ref",
+          "ref": "app.bsky.embed.defs#aspectRatio"
+        }
+      }
+    },
+    "view": {
+      "type": "object",
+      "required": ["images"],
+      "properties": {
+        "images": {
+          "type": "array",
+          "items": { "type": "ref", "ref": "#viewImage" },
+          "maxLength": 4
+        }
+      }
+    },
+    "viewImage": {
+      "type": "object",
+      "required": ["thumb", "fullsize", "alt"],
+      "properties": {
+        "thumb": {
+          "type": "string",
+          "format": "uri",
+          "description": "Fully-qualified URL where a thumbnail of the image can be fetched. For example, CDN location provided by the App View."
+        },
+        "fullsize": {
+          "type": "string",
+          "format": "uri",
+          "description": "Fully-qualified URL where a large version of the image can be fetched. May or may not be the exact original blob. For example, CDN location provided by the App View."
+        },
+        "alt": {
+          "type": "string",
+          "description": "Alt text description of the image, for accessibility."
+        },
+        "aspectRatio": {
+          "type": "ref",
+          "ref": "app.bsky.embed.defs#aspectRatio"
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/embed/record.json b/lexicons/app/bsky/embed/record.json
new file mode 100644
index 00000000..a73200a3
--- /dev/null
+++ b/lexicons/app/bsky/embed/record.json
@@ -0,0 +1,96 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.embed.record",
+  "description": "A representation of a record embedded in a Bluesky record (eg, a post). For example, a quote-post, or sharing a feed generator record.",
+  "defs": {
+    "main": {
+      "type": "object",
+      "required": ["record"],
+      "properties": {
+        "record": { "type": "ref", "ref": "com.atproto.repo.strongRef" }
+      }
+    },
+    "view": {
+      "type": "object",
+      "required": ["record"],
+      "properties": {
+        "record": {
+          "type": "union",
+          "refs": [
+            "#viewRecord",
+            "#viewNotFound",
+            "#viewBlocked",
+            "#viewDetached",
+            "app.bsky.feed.defs#generatorView",
+            "app.bsky.graph.defs#listView",
+            "app.bsky.labeler.defs#labelerView",
+            "app.bsky.graph.defs#starterPackViewBasic"
+          ]
+        }
+      }
+    },
+    "viewRecord": {
+      "type": "object",
+      "required": ["uri", "cid", "author", "value", "indexedAt"],
+      "properties": {
+        "uri": { "type": "string", "format": "at-uri" },
+        "cid": { "type": "string", "format": "cid" },
+        "author": {
+          "type": "ref",
+          "ref": "app.bsky.actor.defs#profileViewBasic"
+        },
+        "value": {
+          "type": "unknown",
+          "description": "The record data itself."
+        },
+        "labels": {
+          "type": "array",
+          "items": { "type": "ref", "ref": "com.atproto.label.defs#label" }
+        },
+        "replyCount": { "type": "integer" },
+        "repostCount": { "type": "integer" },
+        "likeCount": { "type": "integer" },
+        "quoteCount": { "type": "integer" },
+        "embeds": {
+          "type": "array",
+          "items": {
+            "type": "union",
+            "refs": [
+              "app.bsky.embed.images#view",
+              "app.bsky.embed.video#view",
+              "app.bsky.embed.external#view",
+              "app.bsky.embed.record#view",
+              "app.bsky.embed.recordWithMedia#view"
+            ]
+          }
+        },
+        "indexedAt": { "type": "string", "format": "datetime" }
+      }
+    },
+    "viewNotFound": {
+      "type": "object",
+      "required": ["uri", "notFound"],
+      "properties": {
+        "uri": { "type": "string", "format": "at-uri" },
+        "notFound": { "type": "boolean", "const": true }
+      }
+    },
+    "viewBlocked": {
+      "type": "object",
+      "required": ["uri", "blocked", "author"],
+      "properties": {
+        "uri": { "type": "string", "format": "at-uri" },
+        "blocked": { "type": "boolean", "const": true },
+        "author": { "type": "ref", "ref": "app.bsky.feed.defs#blockedAuthor" }
+      }
+    },
+    "viewDetached": {
+      "type": "object",
+      "required": ["uri", "detached"],
+      "properties": {
+        "uri": { "type": "string", "format": "at-uri" },
+        "detached": { "type": "boolean", "const": true }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/embed/recordWithMedia.json b/lexicons/app/bsky/embed/recordWithMedia.json
new file mode 100644
index 00000000..5baf6850
--- /dev/null
+++ b/lexicons/app/bsky/embed/recordWithMedia.json
@@ -0,0 +1,43 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.embed.recordWithMedia",
+  "description": "A representation of a record embedded in a Bluesky record (eg, a post), alongside other compatible embeds. For example, a quote post and image, or a quote post and external URL card.",
+  "defs": {
+    "main": {
+      "type": "object",
+      "required": ["record", "media"],
+      "properties": {
+        "record": {
+          "type": "ref",
+          "ref": "app.bsky.embed.record"
+        },
+        "media": {
+          "type": "union",
+          "refs": [
+            "app.bsky.embed.images",
+            "app.bsky.embed.video",
+            "app.bsky.embed.external"
+          ]
+        }
+      }
+    },
+    "view": {
+      "type": "object",
+      "required": ["record", "media"],
+      "properties": {
+        "record": {
+          "type": "ref",
+          "ref": "app.bsky.embed.record#view"
+        },
+        "media": {
+          "type": "union",
+          "refs": [
+            "app.bsky.embed.images#view",
+            "app.bsky.embed.video#view",
+            "app.bsky.embed.external#view"
+          ]
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/embed/video.json b/lexicons/app/bsky/embed/video.json
new file mode 100644
index 00000000..92511af1
--- /dev/null
+++ b/lexicons/app/bsky/embed/video.json
@@ -0,0 +1,67 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.embed.video",
+  "description": "A video embedded in a Bluesky record (eg, a post).",
+  "defs": {
+    "main": {
+      "type": "object",
+      "required": ["video"],
+      "properties": {
+        "video": {
+          "type": "blob",
+          "description": "The mp4 video file. May be up to 100mb, formerly limited to 50mb.",
+          "accept": ["video/mp4"],
+          "maxSize": 100000000
+        },
+        "captions": {
+          "type": "array",
+          "items": { "type": "ref", "ref": "#caption" },
+          "maxLength": 20
+        },
+        "alt": {
+          "type": "string",
+          "description": "Alt text description of the video, for accessibility.",
+          "maxGraphemes": 1000,
+          "maxLength": 10000
+        },
+        "aspectRatio": {
+          "type": "ref",
+          "ref": "app.bsky.embed.defs#aspectRatio"
+        }
+      }
+    },
+    "caption": {
+      "type": "object",
+      "required": ["lang", "file"],
+      "properties": {
+        "lang": {
+          "type": "string",
+          "format": "language"
+        },
+        "file": {
+          "type": "blob",
+          "accept": ["text/vtt"],
+          "maxSize": 20000
+        }
+      }
+    },
+    "view": {
+      "type": "object",
+      "required": ["cid", "playlist"],
+      "properties": {
+        "cid": { "type": "string", "format": "cid" },
+        "playlist": { "type": "string", "format": "uri" },
+        "thumbnail": { "type": "string", "format": "uri" },
+        "alt": {
+          "type": "string",
+          "maxGraphemes": 1000,
+          "maxLength": 10000
+        },
+        "aspectRatio": {
+          "type": "ref",
+          "ref": "app.bsky.embed.defs#aspectRatio"
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/feed/defs.json b/lexicons/app/bsky/feed/defs.json
new file mode 100644
index 00000000..0a19cf31
--- /dev/null
+++ b/lexicons/app/bsky/feed/defs.json
@@ -0,0 +1,331 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.feed.defs",
+  "defs": {
+    "postView": {
+      "type": "object",
+      "required": ["uri", "cid", "author", "record", "indexedAt"],
+      "properties": {
+        "uri": { "type": "string", "format": "at-uri" },
+        "cid": { "type": "string", "format": "cid" },
+        "author": {
+          "type": "ref",
+          "ref": "app.bsky.actor.defs#profileViewBasic"
+        },
+        "record": { "type": "unknown" },
+        "embed": {
+          "type": "union",
+          "refs": [
+            "app.bsky.embed.images#view",
+            "app.bsky.embed.video#view",
+            "app.bsky.embed.external#view",
+            "app.bsky.embed.record#view",
+            "app.bsky.embed.recordWithMedia#view"
+          ]
+        },
+        "bookmarkCount": { "type": "integer" },
+        "replyCount": { "type": "integer" },
+        "repostCount": { "type": "integer" },
+        "likeCount": { "type": "integer" },
+        "quoteCount": { "type": "integer" },
+        "indexedAt": { "type": "string", "format": "datetime" },
+        "viewer": { "type": "ref", "ref": "#viewerState" },
+        "labels": {
+          "type": "array",
+          "items": { "type": "ref", "ref": "com.atproto.label.defs#label" }
+        },
+        "threadgate": { "type": "ref", "ref": "#threadgateView" },
+        "debug": {
+          "type": "unknown",
+          "description": "Debug information for internal development"
+        }
+      }
+    },
+    "viewerState": {
+      "type": "object",
+      "description": "Metadata about the requesting account's relationship with the subject content. Only has meaningful content for authed requests.",
+      "properties": {
+        "repost": { "type": "string", "format": "at-uri" },
+        "like": { "type": "string", "format": "at-uri" },
+        "bookmarked": { "type": "boolean" },
+        "threadMuted": { "type": "boolean" },
+        "replyDisabled": { "type": "boolean" },
+        "embeddingDisabled": { "type": "boolean" },
+        "pinned": { "type": "boolean" }
+      }
+    },
+    "threadContext": {
+      "type": "object",
+      "description": "Metadata about this post within the context of the thread it is in.",
+      "properties": {
+        "rootAuthorLike": { "type": "string", "format": "at-uri" }
+      }
+    },
+    "feedViewPost": {
+      "type": "object",
+      "required": ["post"],
+      "properties": {
+        "post": { "type": "ref", "ref": "#postView" },
+        "reply": { "type": "ref", "ref": "#replyRef" },
+        "reason": { "type": "union", "refs": ["#reasonRepost", "#reasonPin"] },
+        "feedContext": {
+          "type": "string",
+          "description": "Context provided by feed generator that may be passed back alongside interactions.",
+          "maxLength": 2000
+        },
+        "reqId": {
+          "type": "string",
+          "description": "Unique identifier per request that may be passed back alongside interactions.",
+          "maxLength": 100
+        }
+      }
+    },
+    "replyRef": {
+      "type": "object",
+      "required": ["root", "parent"],
+      "properties": {
+        "root": {
+          "type": "union",
+          "refs": ["#postView", "#notFoundPost", "#blockedPost"]
+        },
+        "parent": {
+          "type": "union",
+          "refs": ["#postView", "#notFoundPost", "#blockedPost"]
+        },
+        "grandparentAuthor": {
+          "type": "ref",
+          "ref": "app.bsky.actor.defs#profileViewBasic",
+          "description": "When parent is a reply to another post, this is the author of that post."
+        }
+      }
+    },
+    "reasonRepost": {
+      "type": "object",
+      "required": ["by", "indexedAt"],
+      "properties": {
+        "by": { "type": "ref", "ref": "app.bsky.actor.defs#profileViewBasic" },
+        "uri": { "type": "string", "format": "at-uri" },
+        "cid": { "type": "string", "format": "cid" },
+        "indexedAt": { "type": "string", "format": "datetime" }
+      }
+    },
+    "reasonPin": {
+      "type": "object",
+      "properties": {}
+    },
+    "threadViewPost": {
+      "type": "object",
+      "required": ["post"],
+      "properties": {
+        "post": { "type": "ref", "ref": "#postView" },
+        "parent": {
+          "type": "union",
+          "refs": ["#threadViewPost", "#notFoundPost", "#blockedPost"]
+        },
+        "replies": {
+          "type": "array",
+          "items": {
+            "type": "union",
+            "refs": ["#threadViewPost", "#notFoundPost", "#blockedPost"]
+          }
+        },
+        "threadContext": { "type": "ref", "ref": "#threadContext" }
+      }
+    },
+    "notFoundPost": {
+      "type": "object",
+      "required": ["uri", "notFound"],
+      "properties": {
+        "uri": { "type": "string", "format": "at-uri" },
+        "notFound": { "type": "boolean", "const": true }
+      }
+    },
+    "blockedPost": {
+      "type": "object",
+      "required": ["uri", "blocked", "author"],
+      "properties": {
+        "uri": { "type": "string", "format": "at-uri" },
+        "blocked": { "type": "boolean", "const": true },
+        "author": { "type": "ref", "ref": "#blockedAuthor" }
+      }
+    },
+    "blockedAuthor": {
+      "type": "object",
+      "required": ["did"],
+      "properties": {
+        "did": { "type": "string", "format": "did" },
+        "viewer": { "type": "ref", "ref": "app.bsky.actor.defs#viewerState" }
+      }
+    },
+    "generatorView": {
+      "type": "object",
+      "required": ["uri", "cid", "did", "creator", "displayName", "indexedAt"],
+      "properties": {
+        "uri": { "type": "string", "format": "at-uri" },
+        "cid": { "type": "string", "format": "cid" },
+        "did": { "type": "string", "format": "did" },
+        "creator": { "type": "ref", "ref": "app.bsky.actor.defs#profileView" },
+        "displayName": { "type": "string" },
+        "description": {
+          "type": "string",
+          "maxGraphemes": 300,
+          "maxLength": 3000
+        },
+        "descriptionFacets": {
+          "type": "array",
+          "items": { "type": "ref", "ref": "app.bsky.richtext.facet" }
+        },
+        "avatar": { "type": "string", "format": "uri" },
+        "likeCount": { "type": "integer", "minimum": 0 },
+        "acceptsInteractions": { "type": "boolean" },
+        "labels": {
+          "type": "array",
+          "items": { "type": "ref", "ref": "com.atproto.label.defs#label" }
+        },
+        "viewer": { "type": "ref", "ref": "#generatorViewerState" },
+        "contentMode": {
+          "type": "string",
+          "knownValues": [
+            "app.bsky.feed.defs#contentModeUnspecified",
+            "app.bsky.feed.defs#contentModeVideo"
+          ]
+        },
+        "indexedAt": { "type": "string", "format": "datetime" }
+      }
+    },
+    "generatorViewerState": {
+      "type": "object",
+      "properties": {
+        "like": { "type": "string", "format": "at-uri" }
+      }
+    },
+    "skeletonFeedPost": {
+      "type": "object",
+      "required": ["post"],
+      "properties": {
+        "post": { "type": "string", "format": "at-uri" },
+        "reason": {
+          "type": "union",
+          "refs": ["#skeletonReasonRepost", "#skeletonReasonPin"]
+        },
+        "feedContext": {
+          "type": "string",
+          "description": "Context that will be passed through to client and may be passed to feed generator back alongside interactions.",
+          "maxLength": 2000
+        }
+      }
+    },
+    "skeletonReasonRepost": {
+      "type": "object",
+      "required": ["repost"],
+      "properties": {
+        "repost": { "type": "string", "format": "at-uri" }
+      }
+    },
+    "skeletonReasonPin": {
+      "type": "object",
+      "properties": {}
+    },
+    "threadgateView": {
+      "type": "object",
+      "properties": {
+        "uri": { "type": "string", "format": "at-uri" },
+        "cid": { "type": "string", "format": "cid" },
+        "record": { "type": "unknown" },
+        "lists": {
+          "type": "array",
+          "items": { "type": "ref", "ref": "app.bsky.graph.defs#listViewBasic" }
+        }
+      }
+    },
+    "interaction": {
+      "type": "object",
+      "properties": {
+        "item": { "type": "string", "format": "at-uri" },
+        "event": {
+          "type": "string",
+          "knownValues": [
+            "app.bsky.feed.defs#requestLess",
+            "app.bsky.feed.defs#requestMore",
+            "app.bsky.feed.defs#clickthroughItem",
+            "app.bsky.feed.defs#clickthroughAuthor",
+            "app.bsky.feed.defs#clickthroughReposter",
+            "app.bsky.feed.defs#clickthroughEmbed",
+            "app.bsky.feed.defs#interactionSeen",
+            "app.bsky.feed.defs#interactionLike",
+            "app.bsky.feed.defs#interactionRepost",
+            "app.bsky.feed.defs#interactionReply",
+            "app.bsky.feed.defs#interactionQuote",
+            "app.bsky.feed.defs#interactionShare"
+          ]
+        },
+        "feedContext": {
+          "type": "string",
+          "description": "Context on a feed item that was originally supplied by the feed generator on getFeedSkeleton.",
+          "maxLength": 2000
+        },
+        "reqId": {
+          "type": "string",
+          "description": "Unique identifier per request that may be passed back alongside interactions.",
+          "maxLength": 100
+        }
+      }
+    },
+    "requestLess": {
+      "type": "token",
+      "description": "Request that less content like the given feed item be shown in the feed"
+    },
+    "requestMore": {
+      "type": "token",
+      "description": "Request that more content like the given feed item be shown in the feed"
+    },
+    "clickthroughItem": {
+      "type": "token",
+      "description": "User clicked through to the feed item"
+    },
+    "clickthroughAuthor": {
+      "type": "token",
+      "description": "User clicked through to the author of the feed item"
+    },
+    "clickthroughReposter": {
+      "type": "token",
+      "description": "User clicked through to the reposter of the feed item"
+    },
+    "clickthroughEmbed": {
+      "type": "token",
+      "description": "User clicked through to the embedded content of the feed item"
+    },
+    "contentModeUnspecified": {
+      "type": "token",
+      "description": "Declares the feed generator returns any types of posts."
+    },
+    "contentModeVideo": {
+      "type": "token",
+      "description": "Declares the feed generator returns posts containing app.bsky.embed.video embeds."
+    },
+    "interactionSeen": {
+      "type": "token",
+      "description": "Feed item was seen by user"
+    },
+    "interactionLike": {
+      "type": "token",
+      "description": "User liked the feed item"
+    },
+    "interactionRepost": {
+      "type": "token",
+      "description": "User reposted the feed item"
+    },
+    "interactionReply": {
+      "type": "token",
+      "description": "User replied to the feed item"
+    },
+    "interactionQuote": {
+      "type": "token",
+      "description": "User quoted the feed item"
+    },
+    "interactionShare": {
+      "type": "token",
+      "description": "User shared the feed item"
+    }
+  }
+}
diff --git a/lexicons/app/bsky/feed/describeFeedGenerator.json b/lexicons/app/bsky/feed/describeFeedGenerator.json
new file mode 100644
index 00000000..0c7b8c86
--- /dev/null
+++ b/lexicons/app/bsky/feed/describeFeedGenerator.json
@@ -0,0 +1,39 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.feed.describeFeedGenerator",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get information about a feed generator, including policies and offered feed URIs. Does not require auth; implemented by Feed Generator services (not App View).",
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["did", "feeds"],
+          "properties": {
+            "did": { "type": "string", "format": "did" },
+            "feeds": {
+              "type": "array",
+              "items": { "type": "ref", "ref": "#feed" }
+            },
+            "links": { "type": "ref", "ref": "#links" }
+          }
+        }
+      }
+    },
+    "feed": {
+      "type": "object",
+      "required": ["uri"],
+      "properties": {
+        "uri": { "type": "string", "format": "at-uri" }
+      }
+    },
+    "links": {
+      "type": "object",
+      "properties": {
+        "privacyPolicy": { "type": "string" },
+        "termsOfService": { "type": "string" }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/feed/generator.json b/lexicons/app/bsky/feed/generator.json
new file mode 100644
index 00000000..fec0c0fa
--- /dev/null
+++ b/lexicons/app/bsky/feed/generator.json
@@ -0,0 +1,54 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.feed.generator",
+  "defs": {
+    "main": {
+      "type": "record",
+      "description": "Record declaring of the existence of a feed generator, and containing metadata about it. The record can exist in any repository.",
+      "key": "any",
+      "record": {
+        "type": "object",
+        "required": ["did", "displayName", "createdAt"],
+        "properties": {
+          "did": { "type": "string", "format": "did" },
+          "displayName": {
+            "type": "string",
+            "maxGraphemes": 24,
+            "maxLength": 240
+          },
+          "description": {
+            "type": "string",
+            "maxGraphemes": 300,
+            "maxLength": 3000
+          },
+          "descriptionFacets": {
+            "type": "array",
+            "items": { "type": "ref", "ref": "app.bsky.richtext.facet" }
+          },
+          "avatar": {
+            "type": "blob",
+            "accept": ["image/png", "image/jpeg"],
+            "maxSize": 1000000
+          },
+          "acceptsInteractions": {
+            "type": "boolean",
+            "description": "Declaration that a feed accepts feedback interactions from a client through app.bsky.feed.sendInteractions"
+          },
+          "labels": {
+            "type": "union",
+            "description": "Self-label values",
+            "refs": ["com.atproto.label.defs#selfLabels"]
+          },
+          "contentMode": {
+            "type": "string",
+            "knownValues": [
+              "app.bsky.feed.defs#contentModeUnspecified",
+              "app.bsky.feed.defs#contentModeVideo"
+            ]
+          },
+          "createdAt": { "type": "string", "format": "datetime" }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/feed/getActorFeeds.json b/lexicons/app/bsky/feed/getActorFeeds.json
new file mode 100644
index 00000000..9a7dae0a
--- /dev/null
+++ b/lexicons/app/bsky/feed/getActorFeeds.json
@@ -0,0 +1,41 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.feed.getActorFeeds",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get a list of feeds (feed generator records) created by the actor (in the actor's repo).",
+      "parameters": {
+        "type": "params",
+        "required": ["actor"],
+        "properties": {
+          "actor": { "type": "string", "format": "at-identifier" },
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 100,
+            "default": 50
+          },
+          "cursor": { "type": "string" }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["feeds"],
+          "properties": {
+            "cursor": { "type": "string" },
+            "feeds": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "app.bsky.feed.defs#generatorView"
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/feed/getActorLikes.json b/lexicons/app/bsky/feed/getActorLikes.json
new file mode 100644
index 00000000..f7c0224b
--- /dev/null
+++ b/lexicons/app/bsky/feed/getActorLikes.json
@@ -0,0 +1,42 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.feed.getActorLikes",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get a list of posts liked by an actor. Requires auth, actor must be the requesting account.",
+      "parameters": {
+        "type": "params",
+        "required": ["actor"],
+        "properties": {
+          "actor": { "type": "string", "format": "at-identifier" },
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 100,
+            "default": 50
+          },
+          "cursor": { "type": "string" }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["feed"],
+          "properties": {
+            "cursor": { "type": "string" },
+            "feed": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "app.bsky.feed.defs#feedViewPost"
+              }
+            }
+          }
+        }
+      },
+      "errors": [{ "name": "BlockedActor" }, { "name": "BlockedByActor" }]
+    }
+  }
+}
diff --git a/lexicons/app/bsky/feed/getAuthorFeed.json b/lexicons/app/bsky/feed/getAuthorFeed.json
new file mode 100644
index 00000000..c9ef8c50
--- /dev/null
+++ b/lexicons/app/bsky/feed/getAuthorFeed.json
@@ -0,0 +1,58 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.feed.getAuthorFeed",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get a view of an actor's 'author feed' (post and reposts by the author). Does not require auth.",
+      "parameters": {
+        "type": "params",
+        "required": ["actor"],
+        "properties": {
+          "actor": { "type": "string", "format": "at-identifier" },
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 100,
+            "default": 50
+          },
+          "cursor": { "type": "string" },
+          "filter": {
+            "type": "string",
+            "description": "Combinations of post/repost types to include in response.",
+            "knownValues": [
+              "posts_with_replies",
+              "posts_no_replies",
+              "posts_with_media",
+              "posts_and_author_threads",
+              "posts_with_video"
+            ],
+            "default": "posts_with_replies"
+          },
+          "includePins": {
+            "type": "boolean",
+            "default": false
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["feed"],
+          "properties": {
+            "cursor": { "type": "string" },
+            "feed": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "app.bsky.feed.defs#feedViewPost"
+              }
+            }
+          }
+        }
+      },
+      "errors": [{ "name": "BlockedActor" }, { "name": "BlockedByActor" }]
+    }
+  }
+}
diff --git a/lexicons/app/bsky/feed/getFeed.json b/lexicons/app/bsky/feed/getFeed.json
new file mode 100644
index 00000000..ada3098b
--- /dev/null
+++ b/lexicons/app/bsky/feed/getFeed.json
@@ -0,0 +1,42 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.feed.getFeed",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get a hydrated feed from an actor's selected feed generator. Implemented by App View.",
+      "parameters": {
+        "type": "params",
+        "required": ["feed"],
+        "properties": {
+          "feed": { "type": "string", "format": "at-uri" },
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 100,
+            "default": 50
+          },
+          "cursor": { "type": "string" }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["feed"],
+          "properties": {
+            "cursor": { "type": "string" },
+            "feed": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "app.bsky.feed.defs#feedViewPost"
+              }
+            }
+          }
+        }
+      },
+      "errors": [{ "name": "UnknownFeed" }]
+    }
+  }
+}
diff --git a/lexicons/app/bsky/feed/getFeedGenerator.json b/lexicons/app/bsky/feed/getFeedGenerator.json
new file mode 100644
index 00000000..190eca85
--- /dev/null
+++ b/lexicons/app/bsky/feed/getFeedGenerator.json
@@ -0,0 +1,42 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.feed.getFeedGenerator",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get information about a feed generator. Implemented by AppView.",
+      "parameters": {
+        "type": "params",
+        "required": ["feed"],
+        "properties": {
+          "feed": {
+            "type": "string",
+            "format": "at-uri",
+            "description": "AT-URI of the feed generator record."
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["view", "isOnline", "isValid"],
+          "properties": {
+            "view": {
+              "type": "ref",
+              "ref": "app.bsky.feed.defs#generatorView"
+            },
+            "isOnline": {
+              "type": "boolean",
+              "description": "Indicates whether the feed generator service has been online recently, or else seems to be inactive."
+            },
+            "isValid": {
+              "type": "boolean",
+              "description": "Indicates whether the feed generator service is compatible with the record declaration."
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/feed/getFeedGenerators.json b/lexicons/app/bsky/feed/getFeedGenerators.json
new file mode 100644
index 00000000..b8e926db
--- /dev/null
+++ b/lexicons/app/bsky/feed/getFeedGenerators.json
@@ -0,0 +1,36 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.feed.getFeedGenerators",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get information about a list of feed generators.",
+      "parameters": {
+        "type": "params",
+        "required": ["feeds"],
+        "properties": {
+          "feeds": {
+            "type": "array",
+            "items": { "type": "string", "format": "at-uri" }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["feeds"],
+          "properties": {
+            "feeds": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "app.bsky.feed.defs#generatorView"
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/feed/getFeedSkeleton.json b/lexicons/app/bsky/feed/getFeedSkeleton.json
new file mode 100644
index 00000000..36e9be99
--- /dev/null
+++ b/lexicons/app/bsky/feed/getFeedSkeleton.json
@@ -0,0 +1,51 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.feed.getFeedSkeleton",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get a skeleton of a feed provided by a feed generator. Auth is optional, depending on provider requirements, and provides the DID of the requester. Implemented by Feed Generator Service.",
+      "parameters": {
+        "type": "params",
+        "required": ["feed"],
+        "properties": {
+          "feed": {
+            "type": "string",
+            "format": "at-uri",
+            "description": "Reference to feed generator record describing the specific feed being requested."
+          },
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 100,
+            "default": 50
+          },
+          "cursor": { "type": "string" }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["feed"],
+          "properties": {
+            "cursor": { "type": "string" },
+            "feed": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "app.bsky.feed.defs#skeletonFeedPost"
+              }
+            },
+            "reqId": {
+              "type": "string",
+              "description": "Unique identifier per request that may be passed back alongside interactions.",
+              "maxLength": 100
+            }
+          }
+        }
+      },
+      "errors": [{ "name": "UnknownFeed" }]
+    }
+  }
+}
diff --git a/lexicons/app/bsky/feed/getLikes.json b/lexicons/app/bsky/feed/getLikes.json
new file mode 100644
index 00000000..d2c5b1a7
--- /dev/null
+++ b/lexicons/app/bsky/feed/getLikes.json
@@ -0,0 +1,58 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.feed.getLikes",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get like records which reference a subject (by AT-URI and CID).",
+      "parameters": {
+        "type": "params",
+        "required": ["uri"],
+        "properties": {
+          "uri": {
+            "type": "string",
+            "format": "at-uri",
+            "description": "AT-URI of the subject (eg, a post record)."
+          },
+          "cid": {
+            "type": "string",
+            "format": "cid",
+            "description": "CID of the subject record (aka, specific version of record), to filter likes."
+          },
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 100,
+            "default": 50
+          },
+          "cursor": { "type": "string" }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["uri", "likes"],
+          "properties": {
+            "uri": { "type": "string", "format": "at-uri" },
+            "cid": { "type": "string", "format": "cid" },
+            "cursor": { "type": "string" },
+            "likes": {
+              "type": "array",
+              "items": { "type": "ref", "ref": "#like" }
+            }
+          }
+        }
+      }
+    },
+    "like": {
+      "type": "object",
+      "required": ["indexedAt", "createdAt", "actor"],
+      "properties": {
+        "indexedAt": { "type": "string", "format": "datetime" },
+        "createdAt": { "type": "string", "format": "datetime" },
+        "actor": { "type": "ref", "ref": "app.bsky.actor.defs#profileView" }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/feed/getListFeed.json b/lexicons/app/bsky/feed/getListFeed.json
new file mode 100644
index 00000000..9dd9fdc7
--- /dev/null
+++ b/lexicons/app/bsky/feed/getListFeed.json
@@ -0,0 +1,46 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.feed.getListFeed",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get a feed of recent posts from a list (posts and reposts from any actors on the list). Does not require auth.",
+      "parameters": {
+        "type": "params",
+        "required": ["list"],
+        "properties": {
+          "list": {
+            "type": "string",
+            "format": "at-uri",
+            "description": "Reference (AT-URI) to the list record."
+          },
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 100,
+            "default": 50
+          },
+          "cursor": { "type": "string" }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["feed"],
+          "properties": {
+            "cursor": { "type": "string" },
+            "feed": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "app.bsky.feed.defs#feedViewPost"
+              }
+            }
+          }
+        }
+      },
+      "errors": [{ "name": "UnknownList" }]
+    }
+  }
+}
diff --git a/lexicons/app/bsky/feed/getPostThread.json b/lexicons/app/bsky/feed/getPostThread.json
new file mode 100644
index 00000000..7efab15f
--- /dev/null
+++ b/lexicons/app/bsky/feed/getPostThread.json
@@ -0,0 +1,57 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.feed.getPostThread",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get posts in a thread. Does not require auth, but additional metadata and filtering will be applied for authed requests.",
+      "parameters": {
+        "type": "params",
+        "required": ["uri"],
+        "properties": {
+          "uri": {
+            "type": "string",
+            "format": "at-uri",
+            "description": "Reference (AT-URI) to post record."
+          },
+          "depth": {
+            "type": "integer",
+            "description": "How many levels of reply depth should be included in response.",
+            "default": 6,
+            "minimum": 0,
+            "maximum": 1000
+          },
+          "parentHeight": {
+            "type": "integer",
+            "description": "How many levels of parent (and grandparent, etc) post to include.",
+            "default": 80,
+            "minimum": 0,
+            "maximum": 1000
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["thread"],
+          "properties": {
+            "thread": {
+              "type": "union",
+              "refs": [
+                "app.bsky.feed.defs#threadViewPost",
+                "app.bsky.feed.defs#notFoundPost",
+                "app.bsky.feed.defs#blockedPost"
+              ]
+            },
+            "threadgate": {
+              "type": "ref",
+              "ref": "app.bsky.feed.defs#threadgateView"
+            }
+          }
+        }
+      },
+      "errors": [{ "name": "NotFound" }]
+    }
+  }
+}
diff --git a/lexicons/app/bsky/feed/getPosts.json b/lexicons/app/bsky/feed/getPosts.json
new file mode 100644
index 00000000..e555ee16
--- /dev/null
+++ b/lexicons/app/bsky/feed/getPosts.json
@@ -0,0 +1,35 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.feed.getPosts",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Gets post views for a specified list of posts (by AT-URI). This is sometimes referred to as 'hydrating' a 'feed skeleton'.",
+      "parameters": {
+        "type": "params",
+        "required": ["uris"],
+        "properties": {
+          "uris": {
+            "type": "array",
+            "description": "List of post AT-URIs to return hydrated views for.",
+            "items": { "type": "string", "format": "at-uri" },
+            "maxLength": 25
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["posts"],
+          "properties": {
+            "posts": {
+              "type": "array",
+              "items": { "type": "ref", "ref": "app.bsky.feed.defs#postView" }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/feed/getQuotes.json b/lexicons/app/bsky/feed/getQuotes.json
new file mode 100644
index 00000000..4ea8b30e
--- /dev/null
+++ b/lexicons/app/bsky/feed/getQuotes.json
@@ -0,0 +1,52 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.feed.getQuotes",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get a list of quotes for a given post.",
+      "parameters": {
+        "type": "params",
+        "required": ["uri"],
+        "properties": {
+          "uri": {
+            "type": "string",
+            "format": "at-uri",
+            "description": "Reference (AT-URI) of post record"
+          },
+          "cid": {
+            "type": "string",
+            "format": "cid",
+            "description": "If supplied, filters to quotes of specific version (by CID) of the post record."
+          },
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 100,
+            "default": 50
+          },
+          "cursor": { "type": "string" }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["uri", "posts"],
+          "properties": {
+            "uri": { "type": "string", "format": "at-uri" },
+            "cid": { "type": "string", "format": "cid" },
+            "cursor": { "type": "string" },
+            "posts": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "app.bsky.feed.defs#postView"
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/feed/getRepostedBy.json b/lexicons/app/bsky/feed/getRepostedBy.json
new file mode 100644
index 00000000..db395346
--- /dev/null
+++ b/lexicons/app/bsky/feed/getRepostedBy.json
@@ -0,0 +1,52 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.feed.getRepostedBy",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get a list of reposts for a given post.",
+      "parameters": {
+        "type": "params",
+        "required": ["uri"],
+        "properties": {
+          "uri": {
+            "type": "string",
+            "format": "at-uri",
+            "description": "Reference (AT-URI) of post record"
+          },
+          "cid": {
+            "type": "string",
+            "format": "cid",
+            "description": "If supplied, filters to reposts of specific version (by CID) of the post record."
+          },
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 100,
+            "default": 50
+          },
+          "cursor": { "type": "string" }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["uri", "repostedBy"],
+          "properties": {
+            "uri": { "type": "string", "format": "at-uri" },
+            "cid": { "type": "string", "format": "cid" },
+            "cursor": { "type": "string" },
+            "repostedBy": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "app.bsky.actor.defs#profileView"
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/feed/getSuggestedFeeds.json b/lexicons/app/bsky/feed/getSuggestedFeeds.json
new file mode 100644
index 00000000..e643d339
--- /dev/null
+++ b/lexicons/app/bsky/feed/getSuggestedFeeds.json
@@ -0,0 +1,39 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.feed.getSuggestedFeeds",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get a list of suggested feeds (feed generators) for the requesting account.",
+      "parameters": {
+        "type": "params",
+        "properties": {
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 100,
+            "default": 50
+          },
+          "cursor": { "type": "string" }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["feeds"],
+          "properties": {
+            "cursor": { "type": "string" },
+            "feeds": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "app.bsky.feed.defs#generatorView"
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/feed/getTimeline.json b/lexicons/app/bsky/feed/getTimeline.json
new file mode 100644
index 00000000..816380fe
--- /dev/null
+++ b/lexicons/app/bsky/feed/getTimeline.json
@@ -0,0 +1,43 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.feed.getTimeline",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get a view of the requesting account's home timeline. This is expected to be some form of reverse-chronological feed.",
+      "parameters": {
+        "type": "params",
+        "properties": {
+          "algorithm": {
+            "type": "string",
+            "description": "Variant 'algorithm' for timeline. Implementation-specific. NOTE: most feed flexibility has been moved to feed generator mechanism."
+          },
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 100,
+            "default": 50
+          },
+          "cursor": { "type": "string" }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["feed"],
+          "properties": {
+            "cursor": { "type": "string" },
+            "feed": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "app.bsky.feed.defs#feedViewPost"
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/feed/like.json b/lexicons/app/bsky/feed/like.json
new file mode 100644
index 00000000..0fcb8039
--- /dev/null
+++ b/lexicons/app/bsky/feed/like.json
@@ -0,0 +1,20 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.feed.like",
+  "defs": {
+    "main": {
+      "type": "record",
+      "description": "Record declaring a 'like' of a piece of subject content.",
+      "key": "tid",
+      "record": {
+        "type": "object",
+        "required": ["subject", "createdAt"],
+        "properties": {
+          "subject": { "type": "ref", "ref": "com.atproto.repo.strongRef" },
+          "createdAt": { "type": "string", "format": "datetime" },
+          "via": { "type": "ref", "ref": "com.atproto.repo.strongRef" }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/feed/post.json b/lexicons/app/bsky/feed/post.json
new file mode 100644
index 00000000..9893a355
--- /dev/null
+++ b/lexicons/app/bsky/feed/post.json
@@ -0,0 +1,96 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.feed.post",
+  "defs": {
+    "main": {
+      "type": "record",
+      "description": "Record containing a Bluesky post.",
+      "key": "tid",
+      "record": {
+        "type": "object",
+        "required": ["text", "createdAt"],
+        "properties": {
+          "text": {
+            "type": "string",
+            "maxLength": 3000,
+            "maxGraphemes": 300,
+            "description": "The primary post content. May be an empty string, if there are embeds."
+          },
+          "entities": {
+            "type": "array",
+            "description": "DEPRECATED: replaced by app.bsky.richtext.facet.",
+            "items": { "type": "ref", "ref": "#entity" }
+          },
+          "facets": {
+            "type": "array",
+            "description": "Annotations of text (mentions, URLs, hashtags, etc)",
+            "items": { "type": "ref", "ref": "app.bsky.richtext.facet" }
+          },
+          "reply": { "type": "ref", "ref": "#replyRef" },
+          "embed": {
+            "type": "union",
+            "refs": [
+              "app.bsky.embed.images",
+              "app.bsky.embed.video",
+              "app.bsky.embed.external",
+              "app.bsky.embed.record",
+              "app.bsky.embed.recordWithMedia"
+            ]
+          },
+          "langs": {
+            "type": "array",
+            "description": "Indicates human language of post primary text content.",
+            "maxLength": 3,
+            "items": { "type": "string", "format": "language" }
+          },
+          "labels": {
+            "type": "union",
+            "description": "Self-label values for this post. Effectively content warnings.",
+            "refs": ["com.atproto.label.defs#selfLabels"]
+          },
+          "tags": {
+            "type": "array",
+            "description": "Additional hashtags, in addition to any included in post text and facets.",
+            "maxLength": 8,
+            "items": { "type": "string", "maxLength": 640, "maxGraphemes": 64 }
+          },
+          "createdAt": {
+            "type": "string",
+            "format": "datetime",
+            "description": "Client-declared timestamp when this post was originally created."
+          }
+        }
+      }
+    },
+    "replyRef": {
+      "type": "object",
+      "required": ["root", "parent"],
+      "properties": {
+        "root": { "type": "ref", "ref": "com.atproto.repo.strongRef" },
+        "parent": { "type": "ref", "ref": "com.atproto.repo.strongRef" }
+      }
+    },
+    "entity": {
+      "type": "object",
+      "description": "Deprecated: use facets instead.",
+      "required": ["index", "type", "value"],
+      "properties": {
+        "index": { "type": "ref", "ref": "#textSlice" },
+        "type": {
+          "type": "string",
+          "description": "Expected values are 'mention' and 'link'."
+        },
+        "value": { "type": "string" }
+      }
+    },
+    "textSlice": {
+      "type": "object",
+      "description": "Deprecated. Use app.bsky.richtext instead -- A text segment. Start is inclusive, end is exclusive. Indices are for utf16-encoded strings.",
+      "required": ["start", "end"],
+      "properties": {
+        "start": { "type": "integer", "minimum": 0 },
+        "end": { "type": "integer", "minimum": 0 }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/feed/postgate.json b/lexicons/app/bsky/feed/postgate.json
new file mode 100644
index 00000000..d98a6731
--- /dev/null
+++ b/lexicons/app/bsky/feed/postgate.json
@@ -0,0 +1,46 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.feed.postgate",
+  "defs": {
+    "main": {
+      "type": "record",
+      "key": "tid",
+      "description": "Record defining interaction rules for a post. The record key (rkey) of the postgate record must match the record key of the post, and that record must be in the same repository.",
+      "record": {
+        "type": "object",
+        "required": ["post", "createdAt"],
+        "properties": {
+          "createdAt": { "type": "string", "format": "datetime" },
+          "post": {
+            "type": "string",
+            "format": "at-uri",
+            "description": "Reference (AT-URI) to the post record."
+          },
+          "detachedEmbeddingUris": {
+            "type": "array",
+            "maxLength": 50,
+            "items": {
+              "type": "string",
+              "format": "at-uri"
+            },
+            "description": "List of AT-URIs embedding this post that the author has detached from."
+          },
+          "embeddingRules": {
+            "description": "List of rules defining who can embed this post. If value is an empty array or is undefined, no particular rules apply and anyone can embed.",
+            "type": "array",
+            "maxLength": 5,
+            "items": {
+              "type": "union",
+              "refs": ["#disableRule"]
+            }
+          }
+        }
+      }
+    },
+    "disableRule": {
+      "type": "object",
+      "description": "Disables embedding of this post.",
+      "properties": {}
+    }
+  }
+}
diff --git a/lexicons/app/bsky/feed/repost.json b/lexicons/app/bsky/feed/repost.json
new file mode 100644
index 00000000..cebb526a
--- /dev/null
+++ b/lexicons/app/bsky/feed/repost.json
@@ -0,0 +1,20 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.feed.repost",
+  "defs": {
+    "main": {
+      "description": "Record representing a 'repost' of an existing Bluesky post.",
+      "type": "record",
+      "key": "tid",
+      "record": {
+        "type": "object",
+        "required": ["subject", "createdAt"],
+        "properties": {
+          "subject": { "type": "ref", "ref": "com.atproto.repo.strongRef" },
+          "createdAt": { "type": "string", "format": "datetime" },
+          "via": { "type": "ref", "ref": "com.atproto.repo.strongRef" }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/feed/searchPosts.json b/lexicons/app/bsky/feed/searchPosts.json
new file mode 100644
index 00000000..c98be44b
--- /dev/null
+++ b/lexicons/app/bsky/feed/searchPosts.json
@@ -0,0 +1,99 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.feed.searchPosts",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Find posts matching search criteria, returning views of those posts. Note that this API endpoint may require authentication (eg, not public) for some service providers and implementations.",
+      "parameters": {
+        "type": "params",
+        "required": ["q"],
+        "properties": {
+          "q": {
+            "type": "string",
+            "description": "Search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended."
+          },
+          "sort": {
+            "type": "string",
+            "knownValues": ["top", "latest"],
+            "default": "latest",
+            "description": "Specifies the ranking order of results."
+          },
+          "since": {
+            "type": "string",
+            "description": "Filter results for posts after the indicated datetime (inclusive). Expected to use 'sortAt' timestamp, which may not match 'createdAt'. Can be a datetime, or just an ISO date (YYYY-MM-DD)."
+          },
+          "until": {
+            "type": "string",
+            "description": "Filter results for posts before the indicated datetime (not inclusive). Expected to use 'sortAt' timestamp, which may not match 'createdAt'. Can be a datetime, or just an ISO date (YYY-MM-DD)."
+          },
+          "mentions": {
+            "type": "string",
+            "format": "at-identifier",
+            "description": "Filter to posts which mention the given account. Handles are resolved to DID before query-time. Only matches rich-text facet mentions."
+          },
+          "author": {
+            "type": "string",
+            "format": "at-identifier",
+            "description": "Filter to posts by the given account. Handles are resolved to DID before query-time."
+          },
+          "lang": {
+            "type": "string",
+            "format": "language",
+            "description": "Filter to posts in the given language. Expected to be based on post language field, though server may override language detection."
+          },
+          "domain": {
+            "type": "string",
+            "description": "Filter to posts with URLs (facet links or embeds) linking to the given domain (hostname). Server may apply hostname normalization."
+          },
+          "url": {
+            "type": "string",
+            "format": "uri",
+            "description": "Filter to posts with links (facet links or embeds) pointing to this URL. Server may apply URL normalization or fuzzy matching."
+          },
+          "tag": {
+            "type": "array",
+            "items": {
+              "type": "string",
+              "maxLength": 640,
+              "maxGraphemes": 64
+            },
+            "description": "Filter to posts with the given tag (hashtag), based on rich-text facet or tag field. Do not include the hash (#) prefix. Multiple tags can be specified, with 'AND' matching."
+          },
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 100,
+            "default": 25
+          },
+          "cursor": {
+            "type": "string",
+            "description": "Optional pagination mechanism; may not necessarily allow scrolling through entire result set."
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["posts"],
+          "properties": {
+            "cursor": { "type": "string" },
+            "hitsTotal": {
+              "type": "integer",
+              "description": "Count of search hits. Optional, may be rounded/truncated, and may not be possible to paginate through all hits."
+            },
+            "posts": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "app.bsky.feed.defs#postView"
+              }
+            }
+          }
+        }
+      },
+      "errors": [{ "name": "BadQueryString" }]
+    }
+  }
+}
diff --git a/lexicons/app/bsky/feed/sendInteractions.json b/lexicons/app/bsky/feed/sendInteractions.json
new file mode 100644
index 00000000..c50fbbc0
--- /dev/null
+++ b/lexicons/app/bsky/feed/sendInteractions.json
@@ -0,0 +1,33 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.feed.sendInteractions",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Send information about interactions with feed items back to the feed generator that served them.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["interactions"],
+          "properties": {
+            "interactions": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "app.bsky.feed.defs#interaction"
+              }
+            }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "properties": {}
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/feed/threadgate.json b/lexicons/app/bsky/feed/threadgate.json
new file mode 100644
index 00000000..b10296c4
--- /dev/null
+++ b/lexicons/app/bsky/feed/threadgate.json
@@ -0,0 +1,69 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.feed.threadgate",
+  "defs": {
+    "main": {
+      "type": "record",
+      "key": "tid",
+      "description": "Record defining interaction gating rules for a thread (aka, reply controls). The record key (rkey) of the threadgate record must match the record key of the thread's root post, and that record must be in the same repository.",
+      "record": {
+        "type": "object",
+        "required": ["post", "createdAt"],
+        "properties": {
+          "post": {
+            "type": "string",
+            "format": "at-uri",
+            "description": "Reference (AT-URI) to the post record."
+          },
+          "allow": {
+            "description": "List of rules defining who can reply to this post. If value is an empty array, no one can reply. If value is undefined, anyone can reply.",
+            "type": "array",
+            "maxLength": 5,
+            "items": {
+              "type": "union",
+              "refs": [
+                "#mentionRule",
+                "#followerRule",
+                "#followingRule",
+                "#listRule"
+              ]
+            }
+          },
+          "createdAt": { "type": "string", "format": "datetime" },
+          "hiddenReplies": {
+            "type": "array",
+            "maxLength": 300,
+            "items": {
+              "type": "string",
+              "format": "at-uri"
+            },
+            "description": "List of hidden reply URIs."
+          }
+        }
+      }
+    },
+    "mentionRule": {
+      "type": "object",
+      "description": "Allow replies from actors mentioned in your post.",
+      "properties": {}
+    },
+    "followerRule": {
+      "type": "object",
+      "description": "Allow replies from actors who follow you.",
+      "properties": {}
+    },
+    "followingRule": {
+      "type": "object",
+      "description": "Allow replies from actors you follow.",
+      "properties": {}
+    },
+    "listRule": {
+      "type": "object",
+      "description": "Allow replies from actors on a list.",
+      "required": ["list"],
+      "properties": {
+        "list": { "type": "string", "format": "at-uri" }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/graph/block.json b/lexicons/app/bsky/graph/block.json
new file mode 100644
index 00000000..b64b11a9
--- /dev/null
+++ b/lexicons/app/bsky/graph/block.json
@@ -0,0 +1,23 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.graph.block",
+  "defs": {
+    "main": {
+      "type": "record",
+      "description": "Record declaring a 'block' relationship against another account. NOTE: blocks are public in Bluesky; see blog posts for details.",
+      "key": "tid",
+      "record": {
+        "type": "object",
+        "required": ["subject", "createdAt"],
+        "properties": {
+          "subject": {
+            "type": "string",
+            "format": "did",
+            "description": "DID of the account to be blocked."
+          },
+          "createdAt": { "type": "string", "format": "datetime" }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/graph/defs.json b/lexicons/app/bsky/graph/defs.json
new file mode 100644
index 00000000..68dc8aa1
--- /dev/null
+++ b/lexicons/app/bsky/graph/defs.json
@@ -0,0 +1,166 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.graph.defs",
+  "defs": {
+    "listViewBasic": {
+      "type": "object",
+      "required": ["uri", "cid", "name", "purpose"],
+      "properties": {
+        "uri": { "type": "string", "format": "at-uri" },
+        "cid": { "type": "string", "format": "cid" },
+        "name": { "type": "string", "maxLength": 64, "minLength": 1 },
+        "purpose": { "type": "ref", "ref": "#listPurpose" },
+        "avatar": { "type": "string", "format": "uri" },
+        "listItemCount": { "type": "integer", "minimum": 0 },
+        "labels": {
+          "type": "array",
+          "items": { "type": "ref", "ref": "com.atproto.label.defs#label" }
+        },
+        "viewer": { "type": "ref", "ref": "#listViewerState" },
+        "indexedAt": { "type": "string", "format": "datetime" }
+      }
+    },
+    "listView": {
+      "type": "object",
+      "required": ["uri", "cid", "creator", "name", "purpose", "indexedAt"],
+      "properties": {
+        "uri": { "type": "string", "format": "at-uri" },
+        "cid": { "type": "string", "format": "cid" },
+        "creator": { "type": "ref", "ref": "app.bsky.actor.defs#profileView" },
+        "name": { "type": "string", "maxLength": 64, "minLength": 1 },
+        "purpose": { "type": "ref", "ref": "#listPurpose" },
+        "description": {
+          "type": "string",
+          "maxGraphemes": 300,
+          "maxLength": 3000
+        },
+        "descriptionFacets": {
+          "type": "array",
+          "items": { "type": "ref", "ref": "app.bsky.richtext.facet" }
+        },
+        "avatar": { "type": "string", "format": "uri" },
+        "listItemCount": { "type": "integer", "minimum": 0 },
+        "labels": {
+          "type": "array",
+          "items": { "type": "ref", "ref": "com.atproto.label.defs#label" }
+        },
+        "viewer": { "type": "ref", "ref": "#listViewerState" },
+        "indexedAt": { "type": "string", "format": "datetime" }
+      }
+    },
+    "listItemView": {
+      "type": "object",
+      "required": ["uri", "subject"],
+      "properties": {
+        "uri": { "type": "string", "format": "at-uri" },
+        "subject": { "type": "ref", "ref": "app.bsky.actor.defs#profileView" }
+      }
+    },
+    "starterPackView": {
+      "type": "object",
+      "required": ["uri", "cid", "record", "creator", "indexedAt"],
+      "properties": {
+        "uri": { "type": "string", "format": "at-uri" },
+        "cid": { "type": "string", "format": "cid" },
+        "record": { "type": "unknown" },
+        "creator": {
+          "type": "ref",
+          "ref": "app.bsky.actor.defs#profileViewBasic"
+        },
+        "list": { "type": "ref", "ref": "#listViewBasic" },
+        "listItemsSample": {
+          "type": "array",
+          "maxLength": 12,
+          "items": { "type": "ref", "ref": "#listItemView" }
+        },
+        "feeds": {
+          "type": "array",
+          "maxLength": 3,
+          "items": { "type": "ref", "ref": "app.bsky.feed.defs#generatorView" }
+        },
+        "joinedWeekCount": { "type": "integer", "minimum": 0 },
+        "joinedAllTimeCount": { "type": "integer", "minimum": 0 },
+        "labels": {
+          "type": "array",
+          "items": { "type": "ref", "ref": "com.atproto.label.defs#label" }
+        },
+        "indexedAt": { "type": "string", "format": "datetime" }
+      }
+    },
+    "starterPackViewBasic": {
+      "type": "object",
+      "required": ["uri", "cid", "record", "creator", "indexedAt"],
+      "properties": {
+        "uri": { "type": "string", "format": "at-uri" },
+        "cid": { "type": "string", "format": "cid" },
+        "record": { "type": "unknown" },
+        "creator": {
+          "type": "ref",
+          "ref": "app.bsky.actor.defs#profileViewBasic"
+        },
+        "listItemCount": { "type": "integer", "minimum": 0 },
+        "joinedWeekCount": { "type": "integer", "minimum": 0 },
+        "joinedAllTimeCount": { "type": "integer", "minimum": 0 },
+        "labels": {
+          "type": "array",
+          "items": { "type": "ref", "ref": "com.atproto.label.defs#label" }
+        },
+        "indexedAt": { "type": "string", "format": "datetime" }
+      }
+    },
+    "listPurpose": {
+      "type": "string",
+      "knownValues": [
+        "app.bsky.graph.defs#modlist",
+        "app.bsky.graph.defs#curatelist",
+        "app.bsky.graph.defs#referencelist"
+      ]
+    },
+    "modlist": {
+      "type": "token",
+      "description": "A list of actors to apply an aggregate moderation action (mute/block) on."
+    },
+    "curatelist": {
+      "type": "token",
+      "description": "A list of actors used for curation purposes such as list feeds or interaction gating."
+    },
+    "referencelist": {
+      "type": "token",
+      "description": "A list of actors used for only for reference purposes such as within a starter pack."
+    },
+    "listViewerState": {
+      "type": "object",
+      "properties": {
+        "muted": { "type": "boolean" },
+        "blocked": { "type": "string", "format": "at-uri" }
+      }
+    },
+    "notFoundActor": {
+      "type": "object",
+      "description": "indicates that a handle or DID could not be resolved",
+      "required": ["actor", "notFound"],
+      "properties": {
+        "actor": { "type": "string", "format": "at-identifier" },
+        "notFound": { "type": "boolean", "const": true }
+      }
+    },
+    "relationship": {
+      "type": "object",
+      "description": "lists the bi-directional graph relationships between one actor (not indicated in the object), and the target actors (the DID included in the object)",
+      "required": ["did"],
+      "properties": {
+        "did": { "type": "string", "format": "did" },
+        "following": {
+          "type": "string",
+          "format": "at-uri",
+          "description": "if the actor follows this DID, this is the AT-URI of the follow record"
+        },
+        "followedBy": {
+          "type": "string",
+          "format": "at-uri",
+          "description": "if the actor is followed by this DID, contains the AT-URI of the follow record"
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/graph/follow.json b/lexicons/app/bsky/graph/follow.json
new file mode 100644
index 00000000..e0b74be9
--- /dev/null
+++ b/lexicons/app/bsky/graph/follow.json
@@ -0,0 +1,20 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.graph.follow",
+  "defs": {
+    "main": {
+      "type": "record",
+      "description": "Record declaring a social 'follow' relationship of another account. Duplicate follows will be ignored by the AppView.",
+      "key": "tid",
+      "record": {
+        "type": "object",
+        "required": ["subject", "createdAt"],
+        "properties": {
+          "subject": { "type": "string", "format": "did" },
+          "createdAt": { "type": "string", "format": "datetime" },
+          "via": { "type": "ref", "ref": "com.atproto.repo.strongRef" }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/graph/getActorStarterPacks.json b/lexicons/app/bsky/graph/getActorStarterPacks.json
new file mode 100644
index 00000000..e6093b40
--- /dev/null
+++ b/lexicons/app/bsky/graph/getActorStarterPacks.json
@@ -0,0 +1,41 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.graph.getActorStarterPacks",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get a list of starter packs created by the actor.",
+      "parameters": {
+        "type": "params",
+        "required": ["actor"],
+        "properties": {
+          "actor": { "type": "string", "format": "at-identifier" },
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 100,
+            "default": 50
+          },
+          "cursor": { "type": "string" }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["starterPacks"],
+          "properties": {
+            "cursor": { "type": "string" },
+            "starterPacks": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "app.bsky.graph.defs#starterPackViewBasic"
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/graph/getBlocks.json b/lexicons/app/bsky/graph/getBlocks.json
new file mode 100644
index 00000000..79a28f66
--- /dev/null
+++ b/lexicons/app/bsky/graph/getBlocks.json
@@ -0,0 +1,39 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.graph.getBlocks",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Enumerates which accounts the requesting account is currently blocking. Requires auth.",
+      "parameters": {
+        "type": "params",
+        "properties": {
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 100,
+            "default": 50
+          },
+          "cursor": { "type": "string" }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["blocks"],
+          "properties": {
+            "cursor": { "type": "string" },
+            "blocks": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "app.bsky.actor.defs#profileView"
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/graph/getFollowers.json b/lexicons/app/bsky/graph/getFollowers.json
new file mode 100644
index 00000000..a6c4facd
--- /dev/null
+++ b/lexicons/app/bsky/graph/getFollowers.json
@@ -0,0 +1,45 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.graph.getFollowers",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Enumerates accounts which follow a specified account (actor).",
+      "parameters": {
+        "type": "params",
+        "required": ["actor"],
+        "properties": {
+          "actor": { "type": "string", "format": "at-identifier" },
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 100,
+            "default": 50
+          },
+          "cursor": { "type": "string" }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["subject", "followers"],
+          "properties": {
+            "subject": {
+              "type": "ref",
+              "ref": "app.bsky.actor.defs#profileView"
+            },
+            "cursor": { "type": "string" },
+            "followers": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "app.bsky.actor.defs#profileView"
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/graph/getFollows.json b/lexicons/app/bsky/graph/getFollows.json
new file mode 100644
index 00000000..81f1b6ab
--- /dev/null
+++ b/lexicons/app/bsky/graph/getFollows.json
@@ -0,0 +1,45 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.graph.getFollows",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Enumerates accounts which a specified account (actor) follows.",
+      "parameters": {
+        "type": "params",
+        "required": ["actor"],
+        "properties": {
+          "actor": { "type": "string", "format": "at-identifier" },
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 100,
+            "default": 50
+          },
+          "cursor": { "type": "string" }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["subject", "follows"],
+          "properties": {
+            "subject": {
+              "type": "ref",
+              "ref": "app.bsky.actor.defs#profileView"
+            },
+            "cursor": { "type": "string" },
+            "follows": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "app.bsky.actor.defs#profileView"
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/graph/getKnownFollowers.json b/lexicons/app/bsky/graph/getKnownFollowers.json
new file mode 100644
index 00000000..dcadba2b
--- /dev/null
+++ b/lexicons/app/bsky/graph/getKnownFollowers.json
@@ -0,0 +1,45 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.graph.getKnownFollowers",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Enumerates accounts which follow a specified account (actor) and are followed by the viewer.",
+      "parameters": {
+        "type": "params",
+        "required": ["actor"],
+        "properties": {
+          "actor": { "type": "string", "format": "at-identifier" },
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 100,
+            "default": 50
+          },
+          "cursor": { "type": "string" }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["subject", "followers"],
+          "properties": {
+            "subject": {
+              "type": "ref",
+              "ref": "app.bsky.actor.defs#profileView"
+            },
+            "cursor": { "type": "string" },
+            "followers": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "app.bsky.actor.defs#profileView"
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/graph/getList.json b/lexicons/app/bsky/graph/getList.json
new file mode 100644
index 00000000..cb95a003
--- /dev/null
+++ b/lexicons/app/bsky/graph/getList.json
@@ -0,0 +1,46 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.graph.getList",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Gets a 'view' (with additional context) of a specified list.",
+      "parameters": {
+        "type": "params",
+        "required": ["list"],
+        "properties": {
+          "list": {
+            "type": "string",
+            "format": "at-uri",
+            "description": "Reference (AT-URI) of the list record to hydrate."
+          },
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 100,
+            "default": 50
+          },
+          "cursor": { "type": "string" }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["list", "items"],
+          "properties": {
+            "cursor": { "type": "string" },
+            "list": { "type": "ref", "ref": "app.bsky.graph.defs#listView" },
+            "items": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "app.bsky.graph.defs#listItemView"
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/graph/getListBlocks.json b/lexicons/app/bsky/graph/getListBlocks.json
new file mode 100644
index 00000000..1bc97661
--- /dev/null
+++ b/lexicons/app/bsky/graph/getListBlocks.json
@@ -0,0 +1,36 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.graph.getListBlocks",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get mod lists that the requesting account (actor) is blocking. Requires auth.",
+      "parameters": {
+        "type": "params",
+        "properties": {
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 100,
+            "default": 50
+          },
+          "cursor": { "type": "string" }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["lists"],
+          "properties": {
+            "cursor": { "type": "string" },
+            "lists": {
+              "type": "array",
+              "items": { "type": "ref", "ref": "app.bsky.graph.defs#listView" }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/graph/getListMutes.json b/lexicons/app/bsky/graph/getListMutes.json
new file mode 100644
index 00000000..a56a8257
--- /dev/null
+++ b/lexicons/app/bsky/graph/getListMutes.json
@@ -0,0 +1,36 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.graph.getListMutes",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Enumerates mod lists that the requesting account (actor) currently has muted. Requires auth.",
+      "parameters": {
+        "type": "params",
+        "properties": {
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 100,
+            "default": 50
+          },
+          "cursor": { "type": "string" }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["lists"],
+          "properties": {
+            "cursor": { "type": "string" },
+            "lists": {
+              "type": "array",
+              "items": { "type": "ref", "ref": "app.bsky.graph.defs#listView" }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/graph/getLists.json b/lexicons/app/bsky/graph/getLists.json
new file mode 100644
index 00000000..5df88dfc
--- /dev/null
+++ b/lexicons/app/bsky/graph/getLists.json
@@ -0,0 +1,50 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.graph.getLists",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Enumerates the lists created by a specified account (actor).",
+      "parameters": {
+        "type": "params",
+        "required": ["actor"],
+        "properties": {
+          "actor": {
+            "type": "string",
+            "format": "at-identifier",
+            "description": "The account (actor) to enumerate lists from."
+          },
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 100,
+            "default": 50
+          },
+          "cursor": { "type": "string" },
+          "purposes": {
+            "type": "array",
+            "description": "Optional filter by list purpose. If not specified, all supported types are returned.",
+            "items": {
+              "type": "string",
+              "knownValues": ["modlist", "curatelist"]
+            }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["lists"],
+          "properties": {
+            "cursor": { "type": "string" },
+            "lists": {
+              "type": "array",
+              "items": { "type": "ref", "ref": "app.bsky.graph.defs#listView" }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/graph/getListsWithMembership.json b/lexicons/app/bsky/graph/getListsWithMembership.json
new file mode 100644
index 00000000..aedec59f
--- /dev/null
+++ b/lexicons/app/bsky/graph/getListsWithMembership.json
@@ -0,0 +1,65 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.graph.getListsWithMembership",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Enumerates the lists created by the session user, and includes membership information about `actor` in those lists. Only supports curation and moderation lists (no reference lists, used in starter packs). Requires auth.",
+      "parameters": {
+        "type": "params",
+        "required": ["actor"],
+        "properties": {
+          "actor": {
+            "type": "string",
+            "format": "at-identifier",
+            "description": "The account (actor) to check for membership."
+          },
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 100,
+            "default": 50
+          },
+          "cursor": { "type": "string" },
+          "purposes": {
+            "type": "array",
+            "description": "Optional filter by list purpose. If not specified, all supported types are returned.",
+            "items": {
+              "type": "string",
+              "knownValues": ["modlist", "curatelist"]
+            }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["listsWithMembership"],
+          "properties": {
+            "cursor": { "type": "string" },
+            "listsWithMembership": {
+              "type": "array",
+              "items": { "type": "ref", "ref": "#listWithMembership" }
+            }
+          }
+        }
+      }
+    },
+    "listWithMembership": {
+      "description": "A list and an optional list item indicating membership of a target user to that list.",
+      "type": "object",
+      "required": ["list"],
+      "properties": {
+        "list": {
+          "type": "ref",
+          "ref": "app.bsky.graph.defs#listView"
+        },
+        "listItem": {
+          "type": "ref",
+          "ref": "app.bsky.graph.defs#listItemView"
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/graph/getMutes.json b/lexicons/app/bsky/graph/getMutes.json
new file mode 100644
index 00000000..22eaf0d3
--- /dev/null
+++ b/lexicons/app/bsky/graph/getMutes.json
@@ -0,0 +1,39 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.graph.getMutes",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Enumerates accounts that the requesting account (actor) currently has muted. Requires auth.",
+      "parameters": {
+        "type": "params",
+        "properties": {
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 100,
+            "default": 50
+          },
+          "cursor": { "type": "string" }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["mutes"],
+          "properties": {
+            "cursor": { "type": "string" },
+            "mutes": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "app.bsky.actor.defs#profileView"
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/graph/getRelationships.json b/lexicons/app/bsky/graph/getRelationships.json
new file mode 100644
index 00000000..03490a25
--- /dev/null
+++ b/lexicons/app/bsky/graph/getRelationships.json
@@ -0,0 +1,56 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.graph.getRelationships",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Enumerates public relationships between one account, and a list of other accounts. Does not require auth.",
+      "parameters": {
+        "type": "params",
+        "required": ["actor"],
+        "properties": {
+          "actor": {
+            "type": "string",
+            "format": "at-identifier",
+            "description": "Primary account requesting relationships for."
+          },
+          "others": {
+            "type": "array",
+            "description": "List of 'other' accounts to be related back to the primary.",
+            "maxLength": 30,
+            "items": {
+              "type": "string",
+              "format": "at-identifier"
+            }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["relationships"],
+          "properties": {
+            "actor": { "type": "string", "format": "did" },
+            "relationships": {
+              "type": "array",
+              "items": {
+                "type": "union",
+                "refs": [
+                  "app.bsky.graph.defs#relationship",
+                  "app.bsky.graph.defs#notFoundActor"
+                ]
+              }
+            }
+          }
+        }
+      },
+      "errors": [
+        {
+          "name": "ActorNotFound",
+          "description": "the primary actor at-identifier could not be resolved"
+        }
+      ]
+    }
+  }
+}
diff --git a/lexicons/app/bsky/graph/getStarterPack.json b/lexicons/app/bsky/graph/getStarterPack.json
new file mode 100644
index 00000000..b4ded4c7
--- /dev/null
+++ b/lexicons/app/bsky/graph/getStarterPack.json
@@ -0,0 +1,34 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.graph.getStarterPack",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Gets a view of a starter pack.",
+      "parameters": {
+        "type": "params",
+        "required": ["starterPack"],
+        "properties": {
+          "starterPack": {
+            "type": "string",
+            "format": "at-uri",
+            "description": "Reference (AT-URI) of the starter pack record."
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["starterPack"],
+          "properties": {
+            "starterPack": {
+              "type": "ref",
+              "ref": "app.bsky.graph.defs#starterPackView"
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/graph/getStarterPacks.json b/lexicons/app/bsky/graph/getStarterPacks.json
new file mode 100644
index 00000000..b57c43ad
--- /dev/null
+++ b/lexicons/app/bsky/graph/getStarterPacks.json
@@ -0,0 +1,37 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.graph.getStarterPacks",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get views for a list of starter packs.",
+      "parameters": {
+        "type": "params",
+        "required": ["uris"],
+        "properties": {
+          "uris": {
+            "type": "array",
+            "items": { "type": "string", "format": "at-uri" },
+            "maxLength": 25
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["starterPacks"],
+          "properties": {
+            "starterPacks": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "app.bsky.graph.defs#starterPackViewBasic"
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/graph/getStarterPacksWithMembership.json b/lexicons/app/bsky/graph/getStarterPacksWithMembership.json
new file mode 100644
index 00000000..914df2fd
--- /dev/null
+++ b/lexicons/app/bsky/graph/getStarterPacksWithMembership.json
@@ -0,0 +1,57 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.graph.getStarterPacksWithMembership",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Enumerates the starter packs created by the session user, and includes membership information about `actor` in those starter packs. Requires auth.",
+      "parameters": {
+        "type": "params",
+        "required": ["actor"],
+        "properties": {
+          "actor": {
+            "type": "string",
+            "format": "at-identifier",
+            "description": "The account (actor) to check for membership."
+          },
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 100,
+            "default": 50
+          },
+          "cursor": { "type": "string" }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["starterPacksWithMembership"],
+          "properties": {
+            "cursor": { "type": "string" },
+            "starterPacksWithMembership": {
+              "type": "array",
+              "items": { "type": "ref", "ref": "#starterPackWithMembership" }
+            }
+          }
+        }
+      }
+    },
+    "starterPackWithMembership": {
+      "description": "A starter pack and an optional list item indicating membership of a target user to that starter pack.",
+      "type": "object",
+      "required": ["starterPack"],
+      "properties": {
+        "starterPack": {
+          "type": "ref",
+          "ref": "app.bsky.graph.defs#starterPackView"
+        },
+        "listItem": {
+          "type": "ref",
+          "ref": "app.bsky.graph.defs#listItemView"
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/graph/getSuggestedFollowsByActor.json b/lexicons/app/bsky/graph/getSuggestedFollowsByActor.json
new file mode 100644
index 00000000..454de154
--- /dev/null
+++ b/lexicons/app/bsky/graph/getSuggestedFollowsByActor.json
@@ -0,0 +1,42 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.graph.getSuggestedFollowsByActor",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Enumerates follows similar to a given account (actor). Expected use is to recommend additional accounts immediately after following one account.",
+      "parameters": {
+        "type": "params",
+        "required": ["actor"],
+        "properties": {
+          "actor": { "type": "string", "format": "at-identifier" }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["suggestions"],
+          "properties": {
+            "suggestions": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "app.bsky.actor.defs#profileView"
+              }
+            },
+            "isFallback": {
+              "type": "boolean",
+              "description": "If true, response has fallen-back to generic results, and is not scoped using relativeToDid",
+              "default": false
+            },
+            "recId": {
+              "type": "integer",
+              "description": "Snowflake for this recommendation, use when submitting recommendation events."
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/graph/list.json b/lexicons/app/bsky/graph/list.json
new file mode 100644
index 00000000..13111412
--- /dev/null
+++ b/lexicons/app/bsky/graph/list.json
@@ -0,0 +1,47 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.graph.list",
+  "defs": {
+    "main": {
+      "type": "record",
+      "description": "Record representing a list of accounts (actors). Scope includes both moderation-oriented lists and curration-oriented lists.",
+      "key": "tid",
+      "record": {
+        "type": "object",
+        "required": ["name", "purpose", "createdAt"],
+        "properties": {
+          "purpose": {
+            "type": "ref",
+            "description": "Defines the purpose of the list (aka, moderation-oriented or curration-oriented)",
+            "ref": "app.bsky.graph.defs#listPurpose"
+          },
+          "name": {
+            "type": "string",
+            "maxLength": 64,
+            "minLength": 1,
+            "description": "Display name for list; can not be empty."
+          },
+          "description": {
+            "type": "string",
+            "maxGraphemes": 300,
+            "maxLength": 3000
+          },
+          "descriptionFacets": {
+            "type": "array",
+            "items": { "type": "ref", "ref": "app.bsky.richtext.facet" }
+          },
+          "avatar": {
+            "type": "blob",
+            "accept": ["image/png", "image/jpeg"],
+            "maxSize": 1000000
+          },
+          "labels": {
+            "type": "union",
+            "refs": ["com.atproto.label.defs#selfLabels"]
+          },
+          "createdAt": { "type": "string", "format": "datetime" }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/graph/listblock.json b/lexicons/app/bsky/graph/listblock.json
new file mode 100644
index 00000000..df2e17f3
--- /dev/null
+++ b/lexicons/app/bsky/graph/listblock.json
@@ -0,0 +1,23 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.graph.listblock",
+  "defs": {
+    "main": {
+      "type": "record",
+      "description": "Record representing a block relationship against an entire an entire list of accounts (actors).",
+      "key": "tid",
+      "record": {
+        "type": "object",
+        "required": ["subject", "createdAt"],
+        "properties": {
+          "subject": {
+            "type": "string",
+            "format": "at-uri",
+            "description": "Reference (AT-URI) to the mod list record."
+          },
+          "createdAt": { "type": "string", "format": "datetime" }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/graph/listitem.json b/lexicons/app/bsky/graph/listitem.json
new file mode 100644
index 00000000..adbd96e7
--- /dev/null
+++ b/lexicons/app/bsky/graph/listitem.json
@@ -0,0 +1,28 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.graph.listitem",
+  "defs": {
+    "main": {
+      "type": "record",
+      "description": "Record representing an account's inclusion on a specific list. The AppView will ignore duplicate listitem records.",
+      "key": "tid",
+      "record": {
+        "type": "object",
+        "required": ["subject", "list", "createdAt"],
+        "properties": {
+          "subject": {
+            "type": "string",
+            "format": "did",
+            "description": "The account which is included on the list."
+          },
+          "list": {
+            "type": "string",
+            "format": "at-uri",
+            "description": "Reference (AT-URI) to the list record (app.bsky.graph.list)."
+          },
+          "createdAt": { "type": "string", "format": "datetime" }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/graph/muteActor.json b/lexicons/app/bsky/graph/muteActor.json
new file mode 100644
index 00000000..c2bf09a3
--- /dev/null
+++ b/lexicons/app/bsky/graph/muteActor.json
@@ -0,0 +1,20 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.graph.muteActor",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Creates a mute relationship for the specified account. Mutes are private in Bluesky. Requires auth.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["actor"],
+          "properties": {
+            "actor": { "type": "string", "format": "at-identifier" }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/graph/muteActorList.json b/lexicons/app/bsky/graph/muteActorList.json
new file mode 100644
index 00000000..ad05e634
--- /dev/null
+++ b/lexicons/app/bsky/graph/muteActorList.json
@@ -0,0 +1,20 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.graph.muteActorList",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Creates a mute relationship for the specified list of accounts. Mutes are private in Bluesky. Requires auth.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["list"],
+          "properties": {
+            "list": { "type": "string", "format": "at-uri" }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/graph/muteThread.json b/lexicons/app/bsky/graph/muteThread.json
new file mode 100644
index 00000000..a20be827
--- /dev/null
+++ b/lexicons/app/bsky/graph/muteThread.json
@@ -0,0 +1,20 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.graph.muteThread",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Mutes a thread preventing notifications from the thread and any of its children. Mutes are private in Bluesky. Requires auth.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["root"],
+          "properties": {
+            "root": { "type": "string", "format": "at-uri" }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/graph/searchStarterPacks.json b/lexicons/app/bsky/graph/searchStarterPacks.json
new file mode 100644
index 00000000..66a26ec4
--- /dev/null
+++ b/lexicons/app/bsky/graph/searchStarterPacks.json
@@ -0,0 +1,48 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.graph.searchStarterPacks",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Find starter packs matching search criteria. Does not require auth.",
+      "parameters": {
+        "type": "params",
+        "required": ["q"],
+        "properties": {
+          "q": {
+            "type": "string",
+            "description": "Search query string. Syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended."
+          },
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 100,
+            "default": 25
+          },
+          "cursor": {
+            "type": "string"
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["starterPacks"],
+          "properties": {
+            "cursor": {
+              "type": "string"
+            },
+            "starterPacks": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "app.bsky.graph.defs#starterPackViewBasic"
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/graph/starterpack.json b/lexicons/app/bsky/graph/starterpack.json
new file mode 100644
index 00000000..17ee55ee
--- /dev/null
+++ b/lexicons/app/bsky/graph/starterpack.json
@@ -0,0 +1,51 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.graph.starterpack",
+  "defs": {
+    "main": {
+      "type": "record",
+      "description": "Record defining a starter pack of actors and feeds for new users.",
+      "key": "tid",
+      "record": {
+        "type": "object",
+        "required": ["name", "list", "createdAt"],
+        "properties": {
+          "name": {
+            "type": "string",
+            "maxGraphemes": 50,
+            "maxLength": 500,
+            "minLength": 1,
+            "description": "Display name for starter pack; can not be empty."
+          },
+          "description": {
+            "type": "string",
+            "maxGraphemes": 300,
+            "maxLength": 3000
+          },
+          "descriptionFacets": {
+            "type": "array",
+            "items": { "type": "ref", "ref": "app.bsky.richtext.facet" }
+          },
+          "list": {
+            "type": "string",
+            "format": "at-uri",
+            "description": "Reference (AT-URI) to the list record."
+          },
+          "feeds": {
+            "type": "array",
+            "maxLength": 3,
+            "items": { "type": "ref", "ref": "#feedItem" }
+          },
+          "createdAt": { "type": "string", "format": "datetime" }
+        }
+      }
+    },
+    "feedItem": {
+      "type": "object",
+      "required": ["uri"],
+      "properties": {
+        "uri": { "type": "string", "format": "at-uri" }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/graph/unmuteActor.json b/lexicons/app/bsky/graph/unmuteActor.json
new file mode 100644
index 00000000..bcea72db
--- /dev/null
+++ b/lexicons/app/bsky/graph/unmuteActor.json
@@ -0,0 +1,20 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.graph.unmuteActor",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Unmutes the specified account. Requires auth.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["actor"],
+          "properties": {
+            "actor": { "type": "string", "format": "at-identifier" }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/graph/unmuteActorList.json b/lexicons/app/bsky/graph/unmuteActorList.json
new file mode 100644
index 00000000..a597838e
--- /dev/null
+++ b/lexicons/app/bsky/graph/unmuteActorList.json
@@ -0,0 +1,20 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.graph.unmuteActorList",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Unmutes the specified list of accounts. Requires auth.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["list"],
+          "properties": {
+            "list": { "type": "string", "format": "at-uri" }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/graph/unmuteThread.json b/lexicons/app/bsky/graph/unmuteThread.json
new file mode 100644
index 00000000..6450a657
--- /dev/null
+++ b/lexicons/app/bsky/graph/unmuteThread.json
@@ -0,0 +1,20 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.graph.unmuteThread",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Unmutes the specified thread. Requires auth.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["root"],
+          "properties": {
+            "root": { "type": "string", "format": "at-uri" }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/graph/verification.json b/lexicons/app/bsky/graph/verification.json
new file mode 100644
index 00000000..407da18e
--- /dev/null
+++ b/lexicons/app/bsky/graph/verification.json
@@ -0,0 +1,36 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.graph.verification",
+  "defs": {
+    "main": {
+      "type": "record",
+      "description": "Record declaring a verification relationship between two accounts. Verifications are only considered valid by an app if issued by an account the app considers trusted.",
+      "key": "tid",
+      "record": {
+        "type": "object",
+        "required": ["subject", "handle", "displayName", "createdAt"],
+        "properties": {
+          "subject": {
+            "description": "DID of the subject the verification applies to.",
+            "type": "string",
+            "format": "did"
+          },
+          "handle": {
+            "description": "Handle of the subject the verification applies to at the moment of verifying, which might not be the same at the time of viewing. The verification is only valid if the current handle matches the one at the time of verifying.",
+            "type": "string",
+            "format": "handle"
+          },
+          "displayName": {
+            "description": "Display name of the subject the verification applies to at the moment of verifying, which might not be the same at the time of viewing. The verification is only valid if the current displayName matches the one at the time of verifying.",
+            "type": "string"
+          },
+          "createdAt": {
+            "description": "Date of when the verification was created.",
+            "type": "string",
+            "format": "datetime"
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/labeler/defs.json b/lexicons/app/bsky/labeler/defs.json
new file mode 100644
index 00000000..b9ef3608
--- /dev/null
+++ b/lexicons/app/bsky/labeler/defs.json
@@ -0,0 +1,91 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.labeler.defs",
+  "defs": {
+    "labelerView": {
+      "type": "object",
+      "required": ["uri", "cid", "creator", "indexedAt"],
+      "properties": {
+        "uri": { "type": "string", "format": "at-uri" },
+        "cid": { "type": "string", "format": "cid" },
+        "creator": { "type": "ref", "ref": "app.bsky.actor.defs#profileView" },
+        "likeCount": { "type": "integer", "minimum": 0 },
+        "viewer": { "type": "ref", "ref": "#labelerViewerState" },
+        "indexedAt": { "type": "string", "format": "datetime" },
+        "labels": {
+          "type": "array",
+          "items": { "type": "ref", "ref": "com.atproto.label.defs#label" }
+        }
+      }
+    },
+    "labelerViewDetailed": {
+      "type": "object",
+      "required": ["uri", "cid", "creator", "policies", "indexedAt"],
+      "properties": {
+        "uri": { "type": "string", "format": "at-uri" },
+        "cid": { "type": "string", "format": "cid" },
+        "creator": { "type": "ref", "ref": "app.bsky.actor.defs#profileView" },
+        "policies": {
+          "type": "ref",
+          "ref": "app.bsky.labeler.defs#labelerPolicies"
+        },
+        "likeCount": { "type": "integer", "minimum": 0 },
+        "viewer": { "type": "ref", "ref": "#labelerViewerState" },
+        "indexedAt": { "type": "string", "format": "datetime" },
+        "labels": {
+          "type": "array",
+          "items": { "type": "ref", "ref": "com.atproto.label.defs#label" }
+        },
+        "reasonTypes": {
+          "description": "The set of report reason 'codes' which are in-scope for this service to review and action. These usually align to policy categories. If not defined (distinct from empty array), all reason types are allowed.",
+          "type": "array",
+          "items": {
+            "type": "ref",
+            "ref": "com.atproto.moderation.defs#reasonType"
+          }
+        },
+        "subjectTypes": {
+          "description": "The set of subject types (account, record, etc) this service accepts reports on.",
+          "type": "array",
+          "items": {
+            "type": "ref",
+            "ref": "com.atproto.moderation.defs#subjectType"
+          }
+        },
+        "subjectCollections": {
+          "type": "array",
+          "description": "Set of record types (collection NSIDs) which can be reported to this service. If not defined (distinct from empty array), default is any record type.",
+          "items": { "type": "string", "format": "nsid" }
+        }
+      }
+    },
+    "labelerViewerState": {
+      "type": "object",
+      "properties": {
+        "like": { "type": "string", "format": "at-uri" }
+      }
+    },
+    "labelerPolicies": {
+      "type": "object",
+      "required": ["labelValues"],
+      "properties": {
+        "labelValues": {
+          "type": "array",
+          "description": "The label values which this labeler publishes. May include global or custom labels.",
+          "items": {
+            "type": "ref",
+            "ref": "com.atproto.label.defs#labelValue"
+          }
+        },
+        "labelValueDefinitions": {
+          "type": "array",
+          "description": "Label values created by this labeler and scoped exclusively to it. Labels defined here will override global label definitions for this labeler.",
+          "items": {
+            "type": "ref",
+            "ref": "com.atproto.label.defs#labelValueDefinition"
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/labeler/getServices.json b/lexicons/app/bsky/labeler/getServices.json
new file mode 100644
index 00000000..df6844e6
--- /dev/null
+++ b/lexicons/app/bsky/labeler/getServices.json
@@ -0,0 +1,43 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.labeler.getServices",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get information about a list of labeler services.",
+      "parameters": {
+        "type": "params",
+        "required": ["dids"],
+        "properties": {
+          "dids": {
+            "type": "array",
+            "items": { "type": "string", "format": "did" }
+          },
+          "detailed": {
+            "type": "boolean",
+            "default": false
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["views"],
+          "properties": {
+            "views": {
+              "type": "array",
+              "items": {
+                "type": "union",
+                "refs": [
+                  "app.bsky.labeler.defs#labelerView",
+                  "app.bsky.labeler.defs#labelerViewDetailed"
+                ]
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/labeler/service.json b/lexicons/app/bsky/labeler/service.json
new file mode 100644
index 00000000..becf1a12
--- /dev/null
+++ b/lexicons/app/bsky/labeler/service.json
@@ -0,0 +1,47 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.labeler.service",
+  "defs": {
+    "main": {
+      "type": "record",
+      "description": "A declaration of the existence of labeler service.",
+      "key": "literal:self",
+      "record": {
+        "type": "object",
+        "required": ["policies", "createdAt"],
+        "properties": {
+          "policies": {
+            "type": "ref",
+            "ref": "app.bsky.labeler.defs#labelerPolicies"
+          },
+          "labels": {
+            "type": "union",
+            "refs": ["com.atproto.label.defs#selfLabels"]
+          },
+          "createdAt": { "type": "string", "format": "datetime" },
+          "reasonTypes": {
+            "description": "The set of report reason 'codes' which are in-scope for this service to review and action. These usually align to policy categories. If not defined (distinct from empty array), all reason types are allowed.",
+            "type": "array",
+            "items": {
+              "type": "ref",
+              "ref": "com.atproto.moderation.defs#reasonType"
+            }
+          },
+          "subjectTypes": {
+            "description": "The set of subject types (account, record, etc) this service accepts reports on.",
+            "type": "array",
+            "items": {
+              "type": "ref",
+              "ref": "com.atproto.moderation.defs#subjectType"
+            }
+          },
+          "subjectCollections": {
+            "type": "array",
+            "description": "Set of record types (collection NSIDs) which can be reported to this service. If not defined (distinct from empty array), default is any record type.",
+            "items": { "type": "string", "format": "nsid" }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/notification/declaration.json b/lexicons/app/bsky/notification/declaration.json
new file mode 100644
index 00000000..6e6013ef
--- /dev/null
+++ b/lexicons/app/bsky/notification/declaration.json
@@ -0,0 +1,22 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.notification.declaration",
+  "defs": {
+    "main": {
+      "type": "record",
+      "description": "A declaration of the user's choices related to notifications that can be produced by them.",
+      "key": "literal:self",
+      "record": {
+        "type": "object",
+        "required": ["allowSubscriptions"],
+        "properties": {
+          "allowSubscriptions": {
+            "type": "string",
+            "description": "A declaration of the user's preference for allowing activity subscriptions from other users. Absence of a record implies 'followers'.",
+            "knownValues": ["followers", "mutuals", "none"]
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/notification/defs.json b/lexicons/app/bsky/notification/defs.json
new file mode 100644
index 00000000..eb077119
--- /dev/null
+++ b/lexicons/app/bsky/notification/defs.json
@@ -0,0 +1,88 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.notification.defs",
+  "defs": {
+    "recordDeleted": {
+      "type": "object",
+      "properties": {}
+    },
+    "chatPreference": {
+      "type": "object",
+      "required": ["include", "push"],
+      "properties": {
+        "include": { "type": "string", "knownValues": ["all", "accepted"] },
+        "push": { "type": "boolean" }
+      }
+    },
+    "filterablePreference": {
+      "type": "object",
+      "required": ["include", "list", "push"],
+      "properties": {
+        "include": { "type": "string", "knownValues": ["all", "follows"] },
+        "list": { "type": "boolean" },
+        "push": { "type": "boolean" }
+      }
+    },
+    "preference": {
+      "type": "object",
+      "required": ["list", "push"],
+      "properties": {
+        "list": { "type": "boolean" },
+        "push": { "type": "boolean" }
+      }
+    },
+    "preferences": {
+      "type": "object",
+      "required": [
+        "chat",
+        "follow",
+        "like",
+        "likeViaRepost",
+        "mention",
+        "quote",
+        "reply",
+        "repost",
+        "repostViaRepost",
+        "starterpackJoined",
+        "subscribedPost",
+        "unverified",
+        "verified"
+      ],
+      "properties": {
+        "chat": { "type": "ref", "ref": "#chatPreference" },
+        "follow": { "type": "ref", "ref": "#filterablePreference" },
+        "like": { "type": "ref", "ref": "#filterablePreference" },
+        "likeViaRepost": { "type": "ref", "ref": "#filterablePreference" },
+        "mention": { "type": "ref", "ref": "#filterablePreference" },
+        "quote": { "type": "ref", "ref": "#filterablePreference" },
+        "reply": { "type": "ref", "ref": "#filterablePreference" },
+        "repost": { "type": "ref", "ref": "#filterablePreference" },
+        "repostViaRepost": { "type": "ref", "ref": "#filterablePreference" },
+        "starterpackJoined": { "type": "ref", "ref": "#preference" },
+        "subscribedPost": { "type": "ref", "ref": "#preference" },
+        "unverified": { "type": "ref", "ref": "#preference" },
+        "verified": { "type": "ref", "ref": "#preference" }
+      }
+    },
+    "activitySubscription": {
+      "type": "object",
+      "required": ["post", "reply"],
+      "properties": {
+        "post": { "type": "boolean" },
+        "reply": { "type": "boolean" }
+      }
+    },
+    "subjectActivitySubscription": {
+      "description": "Object used to store activity subscription data in stash.",
+      "type": "object",
+      "required": ["subject", "activitySubscription"],
+      "properties": {
+        "subject": { "type": "string", "format": "did" },
+        "activitySubscription": {
+          "type": "ref",
+          "ref": "#activitySubscription"
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/notification/getPreferences.json b/lexicons/app/bsky/notification/getPreferences.json
new file mode 100644
index 00000000..b482b3d3
--- /dev/null
+++ b/lexicons/app/bsky/notification/getPreferences.json
@@ -0,0 +1,27 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.notification.getPreferences",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get notification-related preferences for an account. Requires auth.",
+      "parameters": {
+        "type": "params",
+        "properties": {}
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["preferences"],
+          "properties": {
+            "preferences": {
+              "type": "ref",
+              "ref": "app.bsky.notification.defs#preferences"
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/notification/getUnreadCount.json b/lexicons/app/bsky/notification/getUnreadCount.json
new file mode 100644
index 00000000..c5e2df17
--- /dev/null
+++ b/lexicons/app/bsky/notification/getUnreadCount.json
@@ -0,0 +1,27 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.notification.getUnreadCount",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Count the number of unread notifications for the requesting account. Requires auth.",
+      "parameters": {
+        "type": "params",
+        "properties": {
+          "priority": { "type": "boolean" },
+          "seenAt": { "type": "string", "format": "datetime" }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["count"],
+          "properties": {
+            "count": { "type": "integer" }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/notification/listActivitySubscriptions.json b/lexicons/app/bsky/notification/listActivitySubscriptions.json
new file mode 100644
index 00000000..1f408d87
--- /dev/null
+++ b/lexicons/app/bsky/notification/listActivitySubscriptions.json
@@ -0,0 +1,39 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.notification.listActivitySubscriptions",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Enumerate all accounts to which the requesting account is subscribed to receive notifications for. Requires auth.",
+      "parameters": {
+        "type": "params",
+        "properties": {
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 100,
+            "default": 50
+          },
+          "cursor": { "type": "string" }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["subscriptions"],
+          "properties": {
+            "cursor": { "type": "string" },
+            "subscriptions": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "app.bsky.actor.defs#profileView"
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/notification/listNotifications.json b/lexicons/app/bsky/notification/listNotifications.json
new file mode 100644
index 00000000..fdac69ca
--- /dev/null
+++ b/lexicons/app/bsky/notification/listNotifications.json
@@ -0,0 +1,91 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.notification.listNotifications",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Enumerate notifications for the requesting account. Requires auth.",
+      "parameters": {
+        "type": "params",
+        "properties": {
+          "reasons": {
+            "description": "Notification reasons to include in response.",
+            "type": "array",
+            "items": {
+              "type": "string",
+              "description": "A reason that matches the reason property of #notification."
+            }
+          },
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 100,
+            "default": 50
+          },
+          "priority": { "type": "boolean" },
+          "cursor": { "type": "string" },
+          "seenAt": { "type": "string", "format": "datetime" }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["notifications"],
+          "properties": {
+            "cursor": { "type": "string" },
+            "notifications": {
+              "type": "array",
+              "items": { "type": "ref", "ref": "#notification" }
+            },
+            "priority": { "type": "boolean" },
+            "seenAt": { "type": "string", "format": "datetime" }
+          }
+        }
+      }
+    },
+    "notification": {
+      "type": "object",
+      "required": [
+        "uri",
+        "cid",
+        "author",
+        "reason",
+        "record",
+        "isRead",
+        "indexedAt"
+      ],
+      "properties": {
+        "uri": { "type": "string", "format": "at-uri" },
+        "cid": { "type": "string", "format": "cid" },
+        "author": { "type": "ref", "ref": "app.bsky.actor.defs#profileView" },
+        "reason": {
+          "type": "string",
+          "description": "The reason why this notification was delivered - e.g. your post was liked, or you received a new follower.",
+          "knownValues": [
+            "like",
+            "repost",
+            "follow",
+            "mention",
+            "reply",
+            "quote",
+            "starterpack-joined",
+            "verified",
+            "unverified",
+            "like-via-repost",
+            "repost-via-repost",
+            "subscribed-post"
+          ]
+        },
+        "reasonSubject": { "type": "string", "format": "at-uri" },
+        "record": { "type": "unknown" },
+        "isRead": { "type": "boolean" },
+        "indexedAt": { "type": "string", "format": "datetime" },
+        "labels": {
+          "type": "array",
+          "items": { "type": "ref", "ref": "com.atproto.label.defs#label" }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/notification/putActivitySubscription.json b/lexicons/app/bsky/notification/putActivitySubscription.json
new file mode 100644
index 00000000..afc29d59
--- /dev/null
+++ b/lexicons/app/bsky/notification/putActivitySubscription.json
@@ -0,0 +1,38 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.notification.putActivitySubscription",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Puts an activity subscription entry. The key should be omitted for creation and provided for updates. Requires auth.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["subject", "activitySubscription"],
+          "properties": {
+            "subject": { "type": "string", "format": "did" },
+            "activitySubscription": {
+              "type": "ref",
+              "ref": "app.bsky.notification.defs#activitySubscription"
+            }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["subject"],
+          "properties": {
+            "subject": { "type": "string", "format": "did" },
+            "activitySubscription": {
+              "type": "ref",
+              "ref": "app.bsky.notification.defs#activitySubscription"
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/notification/putPreferences.json b/lexicons/app/bsky/notification/putPreferences.json
new file mode 100644
index 00000000..65ae024f
--- /dev/null
+++ b/lexicons/app/bsky/notification/putPreferences.json
@@ -0,0 +1,20 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.notification.putPreferences",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Set notification-related preferences for an account. Requires auth.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["priority"],
+          "properties": {
+            "priority": { "type": "boolean" }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/notification/putPreferencesV2.json b/lexicons/app/bsky/notification/putPreferencesV2.json
new file mode 100644
index 00000000..5a11b95d
--- /dev/null
+++ b/lexicons/app/bsky/notification/putPreferencesV2.json
@@ -0,0 +1,83 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.notification.putPreferencesV2",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Set notification-related preferences for an account. Requires auth.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "properties": {
+            "chat": {
+              "type": "ref",
+              "ref": "app.bsky.notification.defs#chatPreference"
+            },
+            "follow": {
+              "type": "ref",
+              "ref": "app.bsky.notification.defs#filterablePreference"
+            },
+            "like": {
+              "type": "ref",
+              "ref": "app.bsky.notification.defs#filterablePreference"
+            },
+            "likeViaRepost": {
+              "type": "ref",
+              "ref": "app.bsky.notification.defs#filterablePreference"
+            },
+            "mention": {
+              "type": "ref",
+              "ref": "app.bsky.notification.defs#filterablePreference"
+            },
+            "quote": {
+              "type": "ref",
+              "ref": "app.bsky.notification.defs#filterablePreference"
+            },
+            "reply": {
+              "type": "ref",
+              "ref": "app.bsky.notification.defs#filterablePreference"
+            },
+            "repost": {
+              "type": "ref",
+              "ref": "app.bsky.notification.defs#filterablePreference"
+            },
+            "repostViaRepost": {
+              "type": "ref",
+              "ref": "app.bsky.notification.defs#filterablePreference"
+            },
+            "starterpackJoined": {
+              "type": "ref",
+              "ref": "app.bsky.notification.defs#preference"
+            },
+            "subscribedPost": {
+              "type": "ref",
+              "ref": "app.bsky.notification.defs#preference"
+            },
+            "unverified": {
+              "type": "ref",
+              "ref": "app.bsky.notification.defs#preference"
+            },
+            "verified": {
+              "type": "ref",
+              "ref": "app.bsky.notification.defs#preference"
+            }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["preferences"],
+          "properties": {
+            "preferences": {
+              "type": "ref",
+              "ref": "app.bsky.notification.defs#preferences"
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/notification/registerPush.json b/lexicons/app/bsky/notification/registerPush.json
new file mode 100644
index 00000000..6891c877
--- /dev/null
+++ b/lexicons/app/bsky/notification/registerPush.json
@@ -0,0 +1,30 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.notification.registerPush",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Register to receive push notifications, via a specified service, for the requesting account. Requires auth.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["serviceDid", "token", "platform", "appId"],
+          "properties": {
+            "serviceDid": { "type": "string", "format": "did" },
+            "token": { "type": "string" },
+            "platform": {
+              "type": "string",
+              "knownValues": ["ios", "android", "web"]
+            },
+            "appId": { "type": "string" },
+            "ageRestricted": {
+              "type": "boolean",
+              "description": "Set to true when the actor is age restricted"
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/notification/unregisterPush.json b/lexicons/app/bsky/notification/unregisterPush.json
new file mode 100644
index 00000000..b3dcef83
--- /dev/null
+++ b/lexicons/app/bsky/notification/unregisterPush.json
@@ -0,0 +1,26 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.notification.unregisterPush",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "The inverse of registerPush - inform a specified service that push notifications should no longer be sent to the given token for the requesting account. Requires auth.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["serviceDid", "token", "platform", "appId"],
+          "properties": {
+            "serviceDid": { "type": "string", "format": "did" },
+            "token": { "type": "string" },
+            "platform": {
+              "type": "string",
+              "knownValues": ["ios", "android", "web"]
+            },
+            "appId": { "type": "string" }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/notification/updateSeen.json b/lexicons/app/bsky/notification/updateSeen.json
new file mode 100644
index 00000000..84bb0e7d
--- /dev/null
+++ b/lexicons/app/bsky/notification/updateSeen.json
@@ -0,0 +1,20 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.notification.updateSeen",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Notify server that the requesting account has seen notifications. Requires auth.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["seenAt"],
+          "properties": {
+            "seenAt": { "type": "string", "format": "datetime" }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/richtext/facet.json b/lexicons/app/bsky/richtext/facet.json
new file mode 100644
index 00000000..388a3a5e
--- /dev/null
+++ b/lexicons/app/bsky/richtext/facet.json
@@ -0,0 +1,51 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.richtext.facet",
+  "defs": {
+    "main": {
+      "type": "object",
+      "description": "Annotation of a sub-string within rich text.",
+      "required": ["index", "features"],
+      "properties": {
+        "index": { "type": "ref", "ref": "#byteSlice" },
+        "features": {
+          "type": "array",
+          "items": { "type": "union", "refs": ["#mention", "#link", "#tag"] }
+        }
+      }
+    },
+    "mention": {
+      "type": "object",
+      "description": "Facet feature for mention of another account. The text is usually a handle, including a '@' prefix, but the facet reference is a DID.",
+      "required": ["did"],
+      "properties": {
+        "did": { "type": "string", "format": "did" }
+      }
+    },
+    "link": {
+      "type": "object",
+      "description": "Facet feature for a URL. The text URL may have been simplified or truncated, but the facet reference should be a complete URL.",
+      "required": ["uri"],
+      "properties": {
+        "uri": { "type": "string", "format": "uri" }
+      }
+    },
+    "tag": {
+      "type": "object",
+      "description": "Facet feature for a hashtag. The text usually includes a '#' prefix, but the facet reference should not (except in the case of 'double hash tags').",
+      "required": ["tag"],
+      "properties": {
+        "tag": { "type": "string", "maxLength": 640, "maxGraphemes": 64 }
+      }
+    },
+    "byteSlice": {
+      "type": "object",
+      "description": "Specifies the sub-string range a facet feature applies to. Start index is inclusive, end index is exclusive. Indices are zero-indexed, counting bytes of the UTF-8 encoded text. NOTE: some languages, like Javascript, use UTF-16 or Unicode codepoints for string slice indexing; in these languages, convert to byte arrays before working with facets.",
+      "required": ["byteStart", "byteEnd"],
+      "properties": {
+        "byteStart": { "type": "integer", "minimum": 0 },
+        "byteEnd": { "type": "integer", "minimum": 0 }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/unspecced/defs.json b/lexicons/app/bsky/unspecced/defs.json
new file mode 100644
index 00000000..5b6bacca
--- /dev/null
+++ b/lexicons/app/bsky/unspecced/defs.json
@@ -0,0 +1,198 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.unspecced.defs",
+  "defs": {
+    "skeletonSearchPost": {
+      "type": "object",
+      "required": ["uri"],
+      "properties": {
+        "uri": { "type": "string", "format": "at-uri" }
+      }
+    },
+    "skeletonSearchActor": {
+      "type": "object",
+      "required": ["did"],
+      "properties": {
+        "did": { "type": "string", "format": "did" }
+      }
+    },
+    "skeletonSearchStarterPack": {
+      "type": "object",
+      "required": ["uri"],
+      "properties": {
+        "uri": { "type": "string", "format": "at-uri" }
+      }
+    },
+    "trendingTopic": {
+      "type": "object",
+      "required": ["topic", "link"],
+      "properties": {
+        "topic": { "type": "string" },
+        "displayName": { "type": "string" },
+        "description": { "type": "string" },
+        "link": { "type": "string" }
+      }
+    },
+    "skeletonTrend": {
+      "type": "object",
+      "required": [
+        "topic",
+        "displayName",
+        "link",
+        "startedAt",
+        "postCount",
+        "dids"
+      ],
+      "properties": {
+        "topic": { "type": "string" },
+        "displayName": { "type": "string" },
+        "link": { "type": "string" },
+        "startedAt": { "type": "string", "format": "datetime" },
+        "postCount": { "type": "integer" },
+        "status": { "type": "string", "knownValues": ["hot"] },
+        "category": { "type": "string" },
+        "dids": {
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "did"
+          }
+        }
+      }
+    },
+    "trendView": {
+      "type": "object",
+      "required": [
+        "topic",
+        "displayName",
+        "link",
+        "startedAt",
+        "postCount",
+        "actors"
+      ],
+      "properties": {
+        "topic": { "type": "string" },
+        "displayName": { "type": "string" },
+        "link": { "type": "string" },
+        "startedAt": { "type": "string", "format": "datetime" },
+        "postCount": { "type": "integer" },
+        "status": { "type": "string", "knownValues": ["hot"] },
+        "category": { "type": "string" },
+        "actors": {
+          "type": "array",
+          "items": {
+            "type": "ref",
+            "ref": "app.bsky.actor.defs#profileViewBasic"
+          }
+        }
+      }
+    },
+    "threadItemPost": {
+      "type": "object",
+      "required": [
+        "post",
+        "moreParents",
+        "moreReplies",
+        "opThread",
+        "hiddenByThreadgate",
+        "mutedByViewer"
+      ],
+      "properties": {
+        "post": { "type": "ref", "ref": "app.bsky.feed.defs#postView" },
+        "moreParents": {
+          "type": "boolean",
+          "description": "This post has more parents that were not present in the response. This is just a boolean, without the number of parents."
+        },
+        "moreReplies": {
+          "type": "integer",
+          "description": "This post has more replies that were not present in the response. This is a numeric value, which is best-effort and might not be accurate."
+        },
+        "opThread": {
+          "type": "boolean",
+          "description": "This post is part of a contiguous thread by the OP from the thread root. Many different OP threads can happen in the same thread."
+        },
+        "hiddenByThreadgate": {
+          "type": "boolean",
+          "description": "The threadgate created by the author indicates this post as a reply to be hidden for everyone consuming the thread."
+        },
+        "mutedByViewer": {
+          "type": "boolean",
+          "description": "This is by an account muted by the viewer requesting it."
+        }
+      }
+    },
+    "threadItemNoUnauthenticated": {
+      "type": "object",
+      "properties": {}
+    },
+    "threadItemNotFound": {
+      "type": "object",
+      "properties": {}
+    },
+    "threadItemBlocked": {
+      "type": "object",
+      "required": ["author"],
+      "properties": {
+        "author": { "type": "ref", "ref": "app.bsky.feed.defs#blockedAuthor" }
+      }
+    },
+    "ageAssuranceState": {
+      "type": "object",
+      "description": "The computed state of the age assurance process, returned to the user in question on certain authenticated requests.",
+      "required": ["status"],
+      "properties": {
+        "lastInitiatedAt": {
+          "type": "string",
+          "format": "datetime",
+          "description": "The timestamp when this state was last updated."
+        },
+        "status": {
+          "type": "string",
+          "description": "The status of the age assurance process.",
+          "knownValues": ["unknown", "pending", "assured", "blocked"]
+        }
+      }
+    },
+    "ageAssuranceEvent": {
+      "type": "object",
+      "description": "Object used to store age assurance data in stash.",
+      "required": ["createdAt", "status", "attemptId"],
+      "properties": {
+        "createdAt": {
+          "type": "string",
+          "format": "datetime",
+          "description": "The date and time of this write operation."
+        },
+        "status": {
+          "type": "string",
+          "description": "The status of the age assurance process.",
+          "knownValues": ["unknown", "pending", "assured"]
+        },
+        "attemptId": {
+          "type": "string",
+          "description": "The unique identifier for this instance of the age assurance flow, in UUID format."
+        },
+        "email": {
+          "type": "string",
+          "description": "The email used for AA."
+        },
+        "initIp": {
+          "type": "string",
+          "description": "The IP address used when initiating the AA flow."
+        },
+        "initUa": {
+          "type": "string",
+          "description": "The user agent used when initiating the AA flow."
+        },
+        "completeIp": {
+          "type": "string",
+          "description": "The IP address used when completing the AA flow."
+        },
+        "completeUa": {
+          "type": "string",
+          "description": "The user agent used when completing the AA flow."
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/unspecced/getAgeAssuranceState.json b/lexicons/app/bsky/unspecced/getAgeAssuranceState.json
new file mode 100644
index 00000000..3bd42b32
--- /dev/null
+++ b/lexicons/app/bsky/unspecced/getAgeAssuranceState.json
@@ -0,0 +1,17 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.unspecced.getAgeAssuranceState",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Returns the current state of the age assurance process for an account. This is used to check if the user has completed age assurance or if further action is required.",
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "ref",
+          "ref": "app.bsky.unspecced.defs#ageAssuranceState"
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/unspecced/getConfig.json b/lexicons/app/bsky/unspecced/getConfig.json
new file mode 100644
index 00000000..286bc480
--- /dev/null
+++ b/lexicons/app/bsky/unspecced/getConfig.json
@@ -0,0 +1,37 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.unspecced.getConfig",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get miscellaneous runtime configuration.",
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": [],
+          "properties": {
+            "checkEmailConfirmed": { "type": "boolean" },
+            "liveNow": {
+              "type": "array",
+              "items": { "type": "ref", "ref": "#liveNowConfig" }
+            }
+          }
+        }
+      }
+    },
+    "liveNowConfig": {
+      "type": "object",
+      "required": ["did", "domains"],
+      "properties": {
+        "did": { "type": "string", "format": "did" },
+        "domains": {
+          "type": "array",
+          "items": {
+            "type": "string"
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/unspecced/getOnboardingSuggestedStarterPacks.json b/lexicons/app/bsky/unspecced/getOnboardingSuggestedStarterPacks.json
new file mode 100644
index 00000000..60b14901
--- /dev/null
+++ b/lexicons/app/bsky/unspecced/getOnboardingSuggestedStarterPacks.json
@@ -0,0 +1,37 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.unspecced.getOnboardingSuggestedStarterPacks",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get a list of suggested starterpacks for onboarding",
+      "parameters": {
+        "type": "params",
+        "properties": {
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 25,
+            "default": 10
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["starterPacks"],
+          "properties": {
+            "starterPacks": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "app.bsky.graph.defs#starterPackView"
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/unspecced/getOnboardingSuggestedStarterPacksSkeleton.json b/lexicons/app/bsky/unspecced/getOnboardingSuggestedStarterPacksSkeleton.json
new file mode 100644
index 00000000..40f3a762
--- /dev/null
+++ b/lexicons/app/bsky/unspecced/getOnboardingSuggestedStarterPacksSkeleton.json
@@ -0,0 +1,42 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.unspecced.getOnboardingSuggestedStarterPacksSkeleton",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get a skeleton of suggested starterpacks for onboarding. Intended to be called and hydrated by app.bsky.unspecced.getOnboardingSuggestedStarterPacks",
+      "parameters": {
+        "type": "params",
+        "properties": {
+          "viewer": {
+            "type": "string",
+            "format": "did",
+            "description": "DID of the account making the request (not included for public/unauthenticated queries)."
+          },
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 25,
+            "default": 10
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["starterPacks"],
+          "properties": {
+            "starterPacks": {
+              "type": "array",
+              "items": {
+                "type": "string",
+                "format": "at-uri"
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/unspecced/getPopularFeedGenerators.json b/lexicons/app/bsky/unspecced/getPopularFeedGenerators.json
new file mode 100644
index 00000000..ee05c16a
--- /dev/null
+++ b/lexicons/app/bsky/unspecced/getPopularFeedGenerators.json
@@ -0,0 +1,40 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.unspecced.getPopularFeedGenerators",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "An unspecced view of globally popular feed generators.",
+      "parameters": {
+        "type": "params",
+        "properties": {
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 100,
+            "default": 50
+          },
+          "cursor": { "type": "string" },
+          "query": { "type": "string" }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["feeds"],
+          "properties": {
+            "cursor": { "type": "string" },
+            "feeds": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "app.bsky.feed.defs#generatorView"
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/unspecced/getPostThreadOtherV2.json b/lexicons/app/bsky/unspecced/getPostThreadOtherV2.json
new file mode 100644
index 00000000..7fab28bf
--- /dev/null
+++ b/lexicons/app/bsky/unspecced/getPostThreadOtherV2.json
@@ -0,0 +1,56 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.unspecced.getPostThreadOtherV2",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "(NOTE: this endpoint is under development and WILL change without notice. Don't use it until it is moved out of `unspecced` or your application WILL break) Get additional posts under a thread e.g. replies hidden by threadgate. Based on an anchor post at any depth of the tree, returns top-level replies below that anchor. It does not include ancestors nor the anchor itself. This should be called after exhausting `app.bsky.unspecced.getPostThreadV2`. Does not require auth, but additional metadata and filtering will be applied for authed requests.",
+      "parameters": {
+        "type": "params",
+        "required": ["anchor"],
+        "properties": {
+          "anchor": {
+            "type": "string",
+            "format": "at-uri",
+            "description": "Reference (AT-URI) to post record. This is the anchor post."
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["thread"],
+          "properties": {
+            "thread": {
+              "type": "array",
+              "description": "A flat list of other thread items. The depth of each item is indicated by the depth property inside the item.",
+              "items": {
+                "type": "ref",
+                "ref": "#threadItem"
+              }
+            }
+          }
+        }
+      }
+    },
+    "threadItem": {
+      "type": "object",
+      "required": ["uri", "depth", "value"],
+      "properties": {
+        "uri": {
+          "type": "string",
+          "format": "at-uri"
+        },
+        "depth": {
+          "type": "integer",
+          "description": "The nesting level of this item in the thread. Depth 0 means the anchor item. Items above have negative depths, items below have positive depths."
+        },
+        "value": {
+          "type": "union",
+          "refs": ["app.bsky.unspecced.defs#threadItemPost"]
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/unspecced/getPostThreadV2.json b/lexicons/app/bsky/unspecced/getPostThreadV2.json
new file mode 100644
index 00000000..e38dfde9
--- /dev/null
+++ b/lexicons/app/bsky/unspecced/getPostThreadV2.json
@@ -0,0 +1,94 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.unspecced.getPostThreadV2",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "(NOTE: this endpoint is under development and WILL change without notice. Don't use it until it is moved out of `unspecced` or your application WILL break) Get posts in a thread. It is based in an anchor post at any depth of the tree, and returns posts above it (recursively resolving the parent, without further branching to their replies) and below it (recursive replies, with branching to their replies). Does not require auth, but additional metadata and filtering will be applied for authed requests.",
+      "parameters": {
+        "type": "params",
+        "required": ["anchor"],
+        "properties": {
+          "anchor": {
+            "type": "string",
+            "format": "at-uri",
+            "description": "Reference (AT-URI) to post record. This is the anchor post, and the thread will be built around it. It can be any post in the tree, not necessarily a root post."
+          },
+          "above": {
+            "type": "boolean",
+            "description": "Whether to include parents above the anchor.",
+            "default": true
+          },
+          "below": {
+            "type": "integer",
+            "description": "How many levels of replies to include below the anchor.",
+            "default": 6,
+            "minimum": 0,
+            "maximum": 20
+          },
+          "branchingFactor": {
+            "type": "integer",
+            "description": "Maximum of replies to include at each level of the thread, except for the direct replies to the anchor, which are (NOTE: currently, during unspecced phase) all returned (NOTE: later they might be paginated).",
+            "default": 10,
+            "minimum": 0,
+            "maximum": 100
+          },
+          "sort": {
+            "type": "string",
+            "description": "Sorting for the thread replies.",
+            "knownValues": ["newest", "oldest", "top"],
+            "default": "oldest"
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["thread", "hasOtherReplies"],
+          "properties": {
+            "thread": {
+              "type": "array",
+              "description": "A flat list of thread items. The depth of each item is indicated by the depth property inside the item.",
+              "items": {
+                "type": "ref",
+                "ref": "#threadItem"
+              }
+            },
+            "threadgate": {
+              "type": "ref",
+              "ref": "app.bsky.feed.defs#threadgateView"
+            },
+            "hasOtherReplies": {
+              "type": "boolean",
+              "description": "Whether this thread has additional replies. If true, a call can be made to the `getPostThreadOtherV2` endpoint to retrieve them."
+            }
+          }
+        }
+      }
+    },
+    "threadItem": {
+      "type": "object",
+      "required": ["uri", "depth", "value"],
+      "properties": {
+        "uri": {
+          "type": "string",
+          "format": "at-uri"
+        },
+        "depth": {
+          "type": "integer",
+          "description": "The nesting level of this item in the thread. Depth 0 means the anchor item. Items above have negative depths, items below have positive depths."
+        },
+        "value": {
+          "type": "union",
+          "refs": [
+            "app.bsky.unspecced.defs#threadItemPost",
+            "app.bsky.unspecced.defs#threadItemNoUnauthenticated",
+            "app.bsky.unspecced.defs#threadItemNotFound",
+            "app.bsky.unspecced.defs#threadItemBlocked"
+          ]
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/unspecced/getSuggestedFeeds.json b/lexicons/app/bsky/unspecced/getSuggestedFeeds.json
new file mode 100644
index 00000000..1a723a0b
--- /dev/null
+++ b/lexicons/app/bsky/unspecced/getSuggestedFeeds.json
@@ -0,0 +1,37 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.unspecced.getSuggestedFeeds",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get a list of suggested feeds",
+      "parameters": {
+        "type": "params",
+        "properties": {
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 25,
+            "default": 10
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["feeds"],
+          "properties": {
+            "feeds": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "app.bsky.feed.defs#generatorView"
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/unspecced/getSuggestedFeedsSkeleton.json b/lexicons/app/bsky/unspecced/getSuggestedFeedsSkeleton.json
new file mode 100644
index 00000000..c0b72da6
--- /dev/null
+++ b/lexicons/app/bsky/unspecced/getSuggestedFeedsSkeleton.json
@@ -0,0 +1,42 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.unspecced.getSuggestedFeedsSkeleton",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get a skeleton of suggested feeds. Intended to be called and hydrated by app.bsky.unspecced.getSuggestedFeeds",
+      "parameters": {
+        "type": "params",
+        "properties": {
+          "viewer": {
+            "type": "string",
+            "format": "did",
+            "description": "DID of the account making the request (not included for public/unauthenticated queries)."
+          },
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 25,
+            "default": 10
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["feeds"],
+          "properties": {
+            "feeds": {
+              "type": "array",
+              "items": {
+                "type": "string",
+                "format": "at-uri"
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/unspecced/getSuggestedStarterPacks.json b/lexicons/app/bsky/unspecced/getSuggestedStarterPacks.json
new file mode 100644
index 00000000..d4bdd226
--- /dev/null
+++ b/lexicons/app/bsky/unspecced/getSuggestedStarterPacks.json
@@ -0,0 +1,37 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.unspecced.getSuggestedStarterPacks",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get a list of suggested starterpacks",
+      "parameters": {
+        "type": "params",
+        "properties": {
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 25,
+            "default": 10
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["starterPacks"],
+          "properties": {
+            "starterPacks": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "app.bsky.graph.defs#starterPackView"
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/unspecced/getSuggestedStarterPacksSkeleton.json b/lexicons/app/bsky/unspecced/getSuggestedStarterPacksSkeleton.json
new file mode 100644
index 00000000..cf5d88e0
--- /dev/null
+++ b/lexicons/app/bsky/unspecced/getSuggestedStarterPacksSkeleton.json
@@ -0,0 +1,42 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.unspecced.getSuggestedStarterPacksSkeleton",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get a skeleton of suggested starterpacks. Intended to be called and hydrated by app.bsky.unspecced.getSuggestedStarterpacks",
+      "parameters": {
+        "type": "params",
+        "properties": {
+          "viewer": {
+            "type": "string",
+            "format": "did",
+            "description": "DID of the account making the request (not included for public/unauthenticated queries)."
+          },
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 25,
+            "default": 10
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["starterPacks"],
+          "properties": {
+            "starterPacks": {
+              "type": "array",
+              "items": {
+                "type": "string",
+                "format": "at-uri"
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/unspecced/getSuggestedUsers.json b/lexicons/app/bsky/unspecced/getSuggestedUsers.json
new file mode 100644
index 00000000..7a685387
--- /dev/null
+++ b/lexicons/app/bsky/unspecced/getSuggestedUsers.json
@@ -0,0 +1,41 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.unspecced.getSuggestedUsers",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get a list of suggested users",
+      "parameters": {
+        "type": "params",
+        "properties": {
+          "category": {
+            "type": "string",
+            "description": "Category of users to get suggestions for."
+          },
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 50,
+            "default": 25
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["actors"],
+          "properties": {
+            "actors": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "app.bsky.actor.defs#profileView"
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/unspecced/getSuggestedUsersSkeleton.json b/lexicons/app/bsky/unspecced/getSuggestedUsersSkeleton.json
new file mode 100644
index 00000000..d4425621
--- /dev/null
+++ b/lexicons/app/bsky/unspecced/getSuggestedUsersSkeleton.json
@@ -0,0 +1,46 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.unspecced.getSuggestedUsersSkeleton",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get a skeleton of suggested users. Intended to be called and hydrated by app.bsky.unspecced.getSuggestedUsers",
+      "parameters": {
+        "type": "params",
+        "properties": {
+          "viewer": {
+            "type": "string",
+            "format": "did",
+            "description": "DID of the account making the request (not included for public/unauthenticated queries)."
+          },
+          "category": {
+            "type": "string",
+            "description": "Category of users to get suggestions for."
+          },
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 50,
+            "default": 25
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["dids"],
+          "properties": {
+            "dids": {
+              "type": "array",
+              "items": {
+                "type": "string",
+                "format": "did"
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/unspecced/getSuggestionsSkeleton.json b/lexicons/app/bsky/unspecced/getSuggestionsSkeleton.json
new file mode 100644
index 00000000..658f2a11
--- /dev/null
+++ b/lexicons/app/bsky/unspecced/getSuggestionsSkeleton.json
@@ -0,0 +1,58 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.unspecced.getSuggestionsSkeleton",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get a skeleton of suggested actors. Intended to be called and then hydrated through app.bsky.actor.getSuggestions",
+      "parameters": {
+        "type": "params",
+        "properties": {
+          "viewer": {
+            "type": "string",
+            "format": "did",
+            "description": "DID of the account making the request (not included for public/unauthenticated queries). Used to boost followed accounts in ranking."
+          },
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 100,
+            "default": 50
+          },
+          "cursor": { "type": "string" },
+          "relativeToDid": {
+            "type": "string",
+            "format": "did",
+            "description": "DID of the account to get suggestions relative to. If not provided, suggestions will be based on the viewer."
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["actors"],
+          "properties": {
+            "cursor": { "type": "string" },
+            "actors": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "app.bsky.unspecced.defs#skeletonSearchActor"
+              }
+            },
+            "relativeToDid": {
+              "type": "string",
+              "format": "did",
+              "description": "DID of the account these suggestions are relative to. If this is returned undefined, suggestions are based on the viewer."
+            },
+            "recId": {
+              "type": "integer",
+              "description": "Snowflake for this recommendation, use when submitting recommendation events."
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/unspecced/getTaggedSuggestions.json b/lexicons/app/bsky/unspecced/getTaggedSuggestions.json
new file mode 100644
index 00000000..9fd98ffe
--- /dev/null
+++ b/lexicons/app/bsky/unspecced/getTaggedSuggestions.json
@@ -0,0 +1,42 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.unspecced.getTaggedSuggestions",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get a list of suggestions (feeds and users) tagged with categories",
+      "parameters": {
+        "type": "params",
+        "properties": {}
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["suggestions"],
+          "properties": {
+            "suggestions": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "#suggestion"
+              }
+            }
+          }
+        }
+      }
+    },
+    "suggestion": {
+      "type": "object",
+      "required": ["tag", "subjectType", "subject"],
+      "properties": {
+        "tag": { "type": "string" },
+        "subjectType": {
+          "type": "string",
+          "knownValues": ["actor", "feed"]
+        },
+        "subject": { "type": "string", "format": "uri" }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/unspecced/getTrendingTopics.json b/lexicons/app/bsky/unspecced/getTrendingTopics.json
new file mode 100644
index 00000000..097dc1c5
--- /dev/null
+++ b/lexicons/app/bsky/unspecced/getTrendingTopics.json
@@ -0,0 +1,49 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.unspecced.getTrendingTopics",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get a list of trending topics",
+      "parameters": {
+        "type": "params",
+        "properties": {
+          "viewer": {
+            "type": "string",
+            "format": "did",
+            "description": "DID of the account making the request (not included for public/unauthenticated queries). Used to boost followed accounts in ranking."
+          },
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 25,
+            "default": 10
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["topics", "suggested"],
+          "properties": {
+            "topics": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "app.bsky.unspecced.defs#trendingTopic"
+              }
+            },
+            "suggested": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "app.bsky.unspecced.defs#trendingTopic"
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/unspecced/getTrends.json b/lexicons/app/bsky/unspecced/getTrends.json
new file mode 100644
index 00000000..1c114218
--- /dev/null
+++ b/lexicons/app/bsky/unspecced/getTrends.json
@@ -0,0 +1,37 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.unspecced.getTrends",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get the current trends on the network",
+      "parameters": {
+        "type": "params",
+        "properties": {
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 25,
+            "default": 10
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["trends"],
+          "properties": {
+            "trends": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "app.bsky.unspecced.defs#trendView"
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/unspecced/getTrendsSkeleton.json b/lexicons/app/bsky/unspecced/getTrendsSkeleton.json
new file mode 100644
index 00000000..d0b6272b
--- /dev/null
+++ b/lexicons/app/bsky/unspecced/getTrendsSkeleton.json
@@ -0,0 +1,42 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.unspecced.getTrendsSkeleton",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get the skeleton of trends on the network. Intended to be called and then hydrated through app.bsky.unspecced.getTrends",
+      "parameters": {
+        "type": "params",
+        "properties": {
+          "viewer": {
+            "type": "string",
+            "format": "did",
+            "description": "DID of the account making the request (not included for public/unauthenticated queries)."
+          },
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 25,
+            "default": 10
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["trends"],
+          "properties": {
+            "trends": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "app.bsky.unspecced.defs#skeletonTrend"
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/unspecced/initAgeAssurance.json b/lexicons/app/bsky/unspecced/initAgeAssurance.json
new file mode 100644
index 00000000..43c4d3cf
--- /dev/null
+++ b/lexicons/app/bsky/unspecced/initAgeAssurance.json
@@ -0,0 +1,43 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.unspecced.initAgeAssurance",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Initiate age assurance for an account. This is a one-time action that will start the process of verifying the user's age.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["email", "language", "countryCode"],
+          "properties": {
+            "email": {
+              "type": "string",
+              "description": "The user's email address to receive assurance instructions."
+            },
+            "language": {
+              "type": "string",
+              "description": "The user's preferred language for communication during the assurance process."
+            },
+            "countryCode": {
+              "type": "string",
+              "description": "An ISO 3166-1 alpha-2 code of the user's location."
+            }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "ref",
+          "ref": "app.bsky.unspecced.defs#ageAssuranceState"
+        }
+      },
+      "errors": [
+        { "name": "InvalidEmail" },
+        { "name": "DidTooLong" },
+        { "name": "InvalidInitiation" }
+      ]
+    }
+  }
+}
diff --git a/lexicons/app/bsky/unspecced/searchActorsSkeleton.json b/lexicons/app/bsky/unspecced/searchActorsSkeleton.json
new file mode 100644
index 00000000..f6489140
--- /dev/null
+++ b/lexicons/app/bsky/unspecced/searchActorsSkeleton.json
@@ -0,0 +1,61 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.unspecced.searchActorsSkeleton",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Backend Actors (profile) search, returns only skeleton.",
+      "parameters": {
+        "type": "params",
+        "required": ["q"],
+        "properties": {
+          "q": {
+            "type": "string",
+            "description": "Search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended. For typeahead search, only simple term match is supported, not full syntax."
+          },
+          "viewer": {
+            "type": "string",
+            "format": "did",
+            "description": "DID of the account making the request (not included for public/unauthenticated queries). Used to boost followed accounts in ranking."
+          },
+          "typeahead": {
+            "type": "boolean",
+            "description": "If true, acts as fast/simple 'typeahead' query."
+          },
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 100,
+            "default": 25
+          },
+          "cursor": {
+            "type": "string",
+            "description": "Optional pagination mechanism; may not necessarily allow scrolling through entire result set."
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["actors"],
+          "properties": {
+            "cursor": { "type": "string" },
+            "hitsTotal": {
+              "type": "integer",
+              "description": "Count of search hits. Optional, may be rounded/truncated, and may not be possible to paginate through all hits."
+            },
+            "actors": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "app.bsky.unspecced.defs#skeletonSearchActor"
+              }
+            }
+          }
+        }
+      },
+      "errors": [{ "name": "BadQueryString" }]
+    }
+  }
+}
diff --git a/lexicons/app/bsky/unspecced/searchPostsSkeleton.json b/lexicons/app/bsky/unspecced/searchPostsSkeleton.json
new file mode 100644
index 00000000..3be2c1b7
--- /dev/null
+++ b/lexicons/app/bsky/unspecced/searchPostsSkeleton.json
@@ -0,0 +1,104 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.unspecced.searchPostsSkeleton",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Backend Posts search, returns only skeleton",
+      "parameters": {
+        "type": "params",
+        "required": ["q"],
+        "properties": {
+          "q": {
+            "type": "string",
+            "description": "Search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended."
+          },
+          "sort": {
+            "type": "string",
+            "knownValues": ["top", "latest"],
+            "default": "latest",
+            "description": "Specifies the ranking order of results."
+          },
+          "since": {
+            "type": "string",
+            "description": "Filter results for posts after the indicated datetime (inclusive). Expected to use 'sortAt' timestamp, which may not match 'createdAt'. Can be a datetime, or just an ISO date (YYYY-MM-DD)."
+          },
+          "until": {
+            "type": "string",
+            "description": "Filter results for posts before the indicated datetime (not inclusive). Expected to use 'sortAt' timestamp, which may not match 'createdAt'. Can be a datetime, or just an ISO date (YYY-MM-DD)."
+          },
+          "mentions": {
+            "type": "string",
+            "format": "at-identifier",
+            "description": "Filter to posts which mention the given account. Handles are resolved to DID before query-time. Only matches rich-text facet mentions."
+          },
+          "author": {
+            "type": "string",
+            "format": "at-identifier",
+            "description": "Filter to posts by the given account. Handles are resolved to DID before query-time."
+          },
+          "lang": {
+            "type": "string",
+            "format": "language",
+            "description": "Filter to posts in the given language. Expected to be based on post language field, though server may override language detection."
+          },
+          "domain": {
+            "type": "string",
+            "description": "Filter to posts with URLs (facet links or embeds) linking to the given domain (hostname). Server may apply hostname normalization."
+          },
+          "url": {
+            "type": "string",
+            "format": "uri",
+            "description": "Filter to posts with links (facet links or embeds) pointing to this URL. Server may apply URL normalization or fuzzy matching."
+          },
+          "tag": {
+            "type": "array",
+            "items": {
+              "type": "string",
+              "maxLength": 640,
+              "maxGraphemes": 64
+            },
+            "description": "Filter to posts with the given tag (hashtag), based on rich-text facet or tag field. Do not include the hash (#) prefix. Multiple tags can be specified, with 'AND' matching."
+          },
+          "viewer": {
+            "type": "string",
+            "format": "did",
+            "description": "DID of the account making the request (not included for public/unauthenticated queries). Used for 'from:me' queries."
+          },
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 100,
+            "default": 25
+          },
+          "cursor": {
+            "type": "string",
+            "description": "Optional pagination mechanism; may not necessarily allow scrolling through entire result set."
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["posts"],
+          "properties": {
+            "cursor": { "type": "string" },
+            "hitsTotal": {
+              "type": "integer",
+              "description": "Count of search hits. Optional, may be rounded/truncated, and may not be possible to paginate through all hits."
+            },
+            "posts": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "app.bsky.unspecced.defs#skeletonSearchPost"
+              }
+            }
+          }
+        }
+      },
+      "errors": [{ "name": "BadQueryString" }]
+    }
+  }
+}
diff --git a/lexicons/app/bsky/unspecced/searchStarterPacksSkeleton.json b/lexicons/app/bsky/unspecced/searchStarterPacksSkeleton.json
new file mode 100644
index 00000000..80c1b881
--- /dev/null
+++ b/lexicons/app/bsky/unspecced/searchStarterPacksSkeleton.json
@@ -0,0 +1,63 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.unspecced.searchStarterPacksSkeleton",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Backend Starter Pack search, returns only skeleton.",
+      "parameters": {
+        "type": "params",
+        "required": ["q"],
+        "properties": {
+          "q": {
+            "type": "string",
+            "description": "Search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended."
+          },
+          "viewer": {
+            "type": "string",
+            "format": "did",
+            "description": "DID of the account making the request (not included for public/unauthenticated queries)."
+          },
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 100,
+            "default": 25
+          },
+          "cursor": {
+            "type": "string",
+            "description": "Optional pagination mechanism; may not necessarily allow scrolling through entire result set."
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["starterPacks"],
+          "properties": {
+            "cursor": {
+              "type": "string"
+            },
+            "hitsTotal": {
+              "type": "integer",
+              "description": "Count of search hits. Optional, may be rounded/truncated, and may not be possible to paginate through all hits."
+            },
+            "starterPacks": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "app.bsky.unspecced.defs#skeletonSearchStarterPack"
+              }
+            }
+          }
+        }
+      },
+      "errors": [
+        {
+          "name": "BadQueryString"
+        }
+      ]
+    }
+  }
+}
diff --git a/lexicons/app/bsky/video/defs.json b/lexicons/app/bsky/video/defs.json
new file mode 100644
index 00000000..8c846ccb
--- /dev/null
+++ b/lexicons/app/bsky/video/defs.json
@@ -0,0 +1,28 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.video.defs",
+  "defs": {
+    "jobStatus": {
+      "type": "object",
+      "required": ["jobId", "did", "state"],
+      "properties": {
+        "jobId": { "type": "string" },
+        "did": { "type": "string", "format": "did" },
+        "state": {
+          "type": "string",
+          "description": "The state of the video processing job. All values not listed as a known value indicate that the job is in process.",
+          "knownValues": ["JOB_STATE_COMPLETED", "JOB_STATE_FAILED"]
+        },
+        "progress": {
+          "type": "integer",
+          "minimum": 0,
+          "maximum": 100,
+          "description": "Progress within the current processing state."
+        },
+        "blob": { "type": "blob" },
+        "error": { "type": "string" },
+        "message": { "type": "string" }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/video/getJobStatus.json b/lexicons/app/bsky/video/getJobStatus.json
new file mode 100644
index 00000000..ff3076f2
--- /dev/null
+++ b/lexicons/app/bsky/video/getJobStatus.json
@@ -0,0 +1,32 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.video.getJobStatus",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get status details for a video processing job.",
+      "parameters": {
+        "type": "params",
+        "required": ["jobId"],
+        "properties": {
+          "jobId": {
+            "type": "string"
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["jobStatus"],
+          "properties": {
+            "jobStatus": {
+              "type": "ref",
+              "ref": "app.bsky.video.defs#jobStatus"
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/video/getUploadLimits.json b/lexicons/app/bsky/video/getUploadLimits.json
new file mode 100644
index 00000000..bcba18e5
--- /dev/null
+++ b/lexicons/app/bsky/video/getUploadLimits.json
@@ -0,0 +1,24 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.video.getUploadLimits",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get video upload limits for the authenticated user.",
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["canUpload"],
+          "properties": {
+            "canUpload": { "type": "boolean" },
+            "remainingDailyVideos": { "type": "integer" },
+            "remainingDailyBytes": { "type": "integer" },
+            "message": { "type": "string" },
+            "error": { "type": "string" }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/app/bsky/video/uploadVideo.json b/lexicons/app/bsky/video/uploadVideo.json
new file mode 100644
index 00000000..8ffc2646
--- /dev/null
+++ b/lexicons/app/bsky/video/uploadVideo.json
@@ -0,0 +1,26 @@
+{
+  "lexicon": 1,
+  "id": "app.bsky.video.uploadVideo",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Upload a video to be processed then stored on the PDS.",
+      "input": {
+        "encoding": "video/mp4"
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["jobStatus"],
+          "properties": {
+            "jobStatus": {
+              "type": "ref",
+              "ref": "app.bsky.video.defs#jobStatus"
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/chat/bsky/actor/declaration.json b/lexicons/chat/bsky/actor/declaration.json
new file mode 100644
index 00000000..9f2ae0d7
--- /dev/null
+++ b/lexicons/chat/bsky/actor/declaration.json
@@ -0,0 +1,21 @@
+{
+  "lexicon": 1,
+  "id": "chat.bsky.actor.declaration",
+  "defs": {
+    "main": {
+      "type": "record",
+      "description": "A declaration of a Bluesky chat account.",
+      "key": "literal:self",
+      "record": {
+        "type": "object",
+        "required": ["allowIncoming"],
+        "properties": {
+          "allowIncoming": {
+            "type": "string",
+            "knownValues": ["all", "none", "following"]
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/chat/bsky/actor/defs.json b/lexicons/chat/bsky/actor/defs.json
new file mode 100644
index 00000000..fe564beb
--- /dev/null
+++ b/lexicons/chat/bsky/actor/defs.json
@@ -0,0 +1,37 @@
+{
+  "lexicon": 1,
+  "id": "chat.bsky.actor.defs",
+  "defs": {
+    "profileViewBasic": {
+      "type": "object",
+      "required": ["did", "handle"],
+      "properties": {
+        "did": { "type": "string", "format": "did" },
+        "handle": { "type": "string", "format": "handle" },
+        "displayName": {
+          "type": "string",
+          "maxGraphemes": 64,
+          "maxLength": 640
+        },
+        "avatar": { "type": "string", "format": "uri" },
+        "associated": {
+          "type": "ref",
+          "ref": "app.bsky.actor.defs#profileAssociated"
+        },
+        "viewer": { "type": "ref", "ref": "app.bsky.actor.defs#viewerState" },
+        "labels": {
+          "type": "array",
+          "items": { "type": "ref", "ref": "com.atproto.label.defs#label" }
+        },
+        "chatDisabled": {
+          "type": "boolean",
+          "description": "Set to true when the actor cannot actively participate in conversations"
+        },
+        "verification": {
+          "type": "ref",
+          "ref": "app.bsky.actor.defs#verificationState"
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/chat/bsky/actor/deleteAccount.json b/lexicons/chat/bsky/actor/deleteAccount.json
new file mode 100644
index 00000000..11550815
--- /dev/null
+++ b/lexicons/chat/bsky/actor/deleteAccount.json
@@ -0,0 +1,16 @@
+{
+  "lexicon": 1,
+  "id": "chat.bsky.actor.deleteAccount",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "properties": {}
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/chat/bsky/actor/exportAccountData.json b/lexicons/chat/bsky/actor/exportAccountData.json
new file mode 100644
index 00000000..9716aa8f
--- /dev/null
+++ b/lexicons/chat/bsky/actor/exportAccountData.json
@@ -0,0 +1,12 @@
+{
+  "lexicon": 1,
+  "id": "chat.bsky.actor.exportAccountData",
+  "defs": {
+    "main": {
+      "type": "query",
+      "output": {
+        "encoding": "application/jsonl"
+      }
+    }
+  }
+}
diff --git a/lexicons/chat/bsky/convo/acceptConvo.json b/lexicons/chat/bsky/convo/acceptConvo.json
new file mode 100644
index 00000000..a5edbaba
--- /dev/null
+++ b/lexicons/chat/bsky/convo/acceptConvo.json
@@ -0,0 +1,31 @@
+{
+  "lexicon": 1,
+  "id": "chat.bsky.convo.acceptConvo",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["convoId"],
+          "properties": {
+            "convoId": { "type": "string" }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "properties": {
+            "rev": {
+              "description": "Rev when the convo was accepted. If not present, the convo was already accepted.",
+              "type": "string"
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/chat/bsky/convo/addReaction.json b/lexicons/chat/bsky/convo/addReaction.json
new file mode 100644
index 00000000..e8ec68a8
--- /dev/null
+++ b/lexicons/chat/bsky/convo/addReaction.json
@@ -0,0 +1,55 @@
+{
+  "lexicon": 1,
+  "id": "chat.bsky.convo.addReaction",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Adds an emoji reaction to a message. Requires authentication. It is idempotent, so multiple calls from the same user with the same emoji result in a single reaction.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["convoId", "messageId", "value"],
+          "properties": {
+            "convoId": { "type": "string" },
+            "messageId": { "type": "string" },
+            "value": {
+              "type": "string",
+              "minLength": 1,
+              "maxLength": 64,
+              "minGraphemes": 1,
+              "maxGraphemes": 1
+            }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["message"],
+          "properties": {
+            "message": {
+              "type": "ref",
+              "ref": "chat.bsky.convo.defs#messageView"
+            }
+          }
+        }
+      },
+      "errors": [
+        {
+          "name": "ReactionMessageDeleted",
+          "description": "Indicates that the message has been deleted and reactions can no longer be added/removed."
+        },
+        {
+          "name": "ReactionLimitReached",
+          "description": "Indicates that the message has the maximum number of reactions allowed for a single user, and the requested reaction wasn't yet present. If it was already present, the request will not fail since it is idempotent."
+        },
+        {
+          "name": "ReactionInvalidValue",
+          "description": "Indicates the value for the reaction is not acceptable. In general, this means it is not an emoji."
+        }
+      ]
+    }
+  }
+}
diff --git a/lexicons/chat/bsky/convo/defs.json b/lexicons/chat/bsky/convo/defs.json
new file mode 100644
index 00000000..6cbe294a
--- /dev/null
+++ b/lexicons/chat/bsky/convo/defs.json
@@ -0,0 +1,236 @@
+{
+  "lexicon": 1,
+  "id": "chat.bsky.convo.defs",
+  "defs": {
+    "messageRef": {
+      "type": "object",
+      "required": ["did", "messageId", "convoId"],
+      "properties": {
+        "did": { "type": "string", "format": "did" },
+        "convoId": { "type": "string" },
+        "messageId": { "type": "string" }
+      }
+    },
+    "messageInput": {
+      "type": "object",
+      "required": ["text"],
+      "properties": {
+        "text": {
+          "type": "string",
+          "maxLength": 10000,
+          "maxGraphemes": 1000
+        },
+        "facets": {
+          "type": "array",
+          "description": "Annotations of text (mentions, URLs, hashtags, etc)",
+          "items": { "type": "ref", "ref": "app.bsky.richtext.facet" }
+        },
+        "embed": {
+          "type": "union",
+          "refs": ["app.bsky.embed.record"]
+        }
+      }
+    },
+    "messageView": {
+      "type": "object",
+      "required": ["id", "rev", "text", "sender", "sentAt"],
+      "properties": {
+        "id": { "type": "string" },
+        "rev": { "type": "string" },
+        "text": {
+          "type": "string",
+          "maxLength": 10000,
+          "maxGraphemes": 1000
+        },
+        "facets": {
+          "type": "array",
+          "description": "Annotations of text (mentions, URLs, hashtags, etc)",
+          "items": { "type": "ref", "ref": "app.bsky.richtext.facet" }
+        },
+        "embed": {
+          "type": "union",
+          "refs": ["app.bsky.embed.record#view"]
+        },
+        "reactions": {
+          "type": "array",
+          "description": "Reactions to this message, in ascending order of creation time.",
+          "items": { "type": "ref", "ref": "#reactionView" }
+        },
+        "sender": { "type": "ref", "ref": "#messageViewSender" },
+        "sentAt": { "type": "string", "format": "datetime" }
+      }
+    },
+    "deletedMessageView": {
+      "type": "object",
+      "required": ["id", "rev", "sender", "sentAt"],
+      "properties": {
+        "id": { "type": "string" },
+        "rev": { "type": "string" },
+        "sender": { "type": "ref", "ref": "#messageViewSender" },
+        "sentAt": { "type": "string", "format": "datetime" }
+      }
+    },
+    "messageViewSender": {
+      "type": "object",
+      "required": ["did"],
+      "properties": {
+        "did": { "type": "string", "format": "did" }
+      }
+    },
+    "reactionView": {
+      "type": "object",
+      "required": ["value", "sender", "createdAt"],
+      "properties": {
+        "value": { "type": "string" },
+        "sender": { "type": "ref", "ref": "#reactionViewSender" },
+        "createdAt": { "type": "string", "format": "datetime" }
+      }
+    },
+    "reactionViewSender": {
+      "type": "object",
+      "required": ["did"],
+      "properties": {
+        "did": { "type": "string", "format": "did" }
+      }
+    },
+    "messageAndReactionView": {
+      "type": "object",
+      "required": ["message", "reaction"],
+      "properties": {
+        "message": { "type": "ref", "ref": "#messageView" },
+        "reaction": { "type": "ref", "ref": "#reactionView" }
+      }
+    },
+    "convoView": {
+      "type": "object",
+      "required": ["id", "rev", "members", "muted", "unreadCount"],
+      "properties": {
+        "id": { "type": "string" },
+        "rev": { "type": "string" },
+        "members": {
+          "type": "array",
+          "items": {
+            "type": "ref",
+            "ref": "chat.bsky.actor.defs#profileViewBasic"
+          }
+        },
+        "lastMessage": {
+          "type": "union",
+          "refs": ["#messageView", "#deletedMessageView"]
+        },
+        "lastReaction": {
+          "type": "union",
+          "refs": ["#messageAndReactionView"]
+        },
+        "muted": { "type": "boolean" },
+        "status": {
+          "type": "string",
+          "knownValues": ["request", "accepted"]
+        },
+        "unreadCount": { "type": "integer" }
+      }
+    },
+    "logBeginConvo": {
+      "type": "object",
+      "required": ["rev", "convoId"],
+      "properties": {
+        "rev": { "type": "string" },
+        "convoId": { "type": "string" }
+      }
+    },
+    "logAcceptConvo": {
+      "type": "object",
+      "required": ["rev", "convoId"],
+      "properties": {
+        "rev": { "type": "string" },
+        "convoId": { "type": "string" }
+      }
+    },
+    "logLeaveConvo": {
+      "type": "object",
+      "required": ["rev", "convoId"],
+      "properties": {
+        "rev": { "type": "string" },
+        "convoId": { "type": "string" }
+      }
+    },
+    "logMuteConvo": {
+      "type": "object",
+      "required": ["rev", "convoId"],
+      "properties": {
+        "rev": { "type": "string" },
+        "convoId": { "type": "string" }
+      }
+    },
+    "logUnmuteConvo": {
+      "type": "object",
+      "required": ["rev", "convoId"],
+      "properties": {
+        "rev": { "type": "string" },
+        "convoId": { "type": "string" }
+      }
+    },
+    "logCreateMessage": {
+      "type": "object",
+      "required": ["rev", "convoId", "message"],
+      "properties": {
+        "rev": { "type": "string" },
+        "convoId": { "type": "string" },
+        "message": {
+          "type": "union",
+          "refs": ["#messageView", "#deletedMessageView"]
+        }
+      }
+    },
+    "logDeleteMessage": {
+      "type": "object",
+      "required": ["rev", "convoId", "message"],
+      "properties": {
+        "rev": { "type": "string" },
+        "convoId": { "type": "string" },
+        "message": {
+          "type": "union",
+          "refs": ["#messageView", "#deletedMessageView"]
+        }
+      }
+    },
+    "logReadMessage": {
+      "type": "object",
+      "required": ["rev", "convoId", "message"],
+      "properties": {
+        "rev": { "type": "string" },
+        "convoId": { "type": "string" },
+        "message": {
+          "type": "union",
+          "refs": ["#messageView", "#deletedMessageView"]
+        }
+      }
+    },
+    "logAddReaction": {
+      "type": "object",
+      "required": ["rev", "convoId", "message", "reaction"],
+      "properties": {
+        "rev": { "type": "string" },
+        "convoId": { "type": "string" },
+        "message": {
+          "type": "union",
+          "refs": ["#messageView", "#deletedMessageView"]
+        },
+        "reaction": { "type": "ref", "ref": "#reactionView" }
+      }
+    },
+    "logRemoveReaction": {
+      "type": "object",
+      "required": ["rev", "convoId", "message", "reaction"],
+      "properties": {
+        "rev": { "type": "string" },
+        "convoId": { "type": "string" },
+        "message": {
+          "type": "union",
+          "refs": ["#messageView", "#deletedMessageView"]
+        },
+        "reaction": { "type": "ref", "ref": "#reactionView" }
+      }
+    }
+  }
+}
diff --git a/lexicons/chat/bsky/convo/deleteMessageForSelf.json b/lexicons/chat/bsky/convo/deleteMessageForSelf.json
new file mode 100644
index 00000000..dcb814b5
--- /dev/null
+++ b/lexicons/chat/bsky/convo/deleteMessageForSelf.json
@@ -0,0 +1,27 @@
+{
+  "lexicon": 1,
+  "id": "chat.bsky.convo.deleteMessageForSelf",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["convoId", "messageId"],
+          "properties": {
+            "convoId": { "type": "string" },
+            "messageId": { "type": "string" }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "ref",
+          "ref": "chat.bsky.convo.defs#deletedMessageView"
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/chat/bsky/convo/getConvo.json b/lexicons/chat/bsky/convo/getConvo.json
new file mode 100644
index 00000000..5e3e6660
--- /dev/null
+++ b/lexicons/chat/bsky/convo/getConvo.json
@@ -0,0 +1,29 @@
+{
+  "lexicon": 1,
+  "id": "chat.bsky.convo.getConvo",
+  "defs": {
+    "main": {
+      "type": "query",
+      "parameters": {
+        "type": "params",
+        "required": ["convoId"],
+        "properties": {
+          "convoId": { "type": "string" }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["convo"],
+          "properties": {
+            "convo": {
+              "type": "ref",
+              "ref": "chat.bsky.convo.defs#convoView"
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/chat/bsky/convo/getConvoAvailability.json b/lexicons/chat/bsky/convo/getConvoAvailability.json
new file mode 100644
index 00000000..b7084f25
--- /dev/null
+++ b/lexicons/chat/bsky/convo/getConvoAvailability.json
@@ -0,0 +1,41 @@
+{
+  "lexicon": 1,
+  "id": "chat.bsky.convo.getConvoAvailability",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get whether the requester and the other members can chat. If an existing convo is found for these members, it is returned.",
+      "parameters": {
+        "type": "params",
+        "required": ["members"],
+        "properties": {
+          "members": {
+            "type": "array",
+            "minLength": 1,
+            "maxLength": 10,
+            "items": {
+              "type": "string",
+              "format": "did"
+            }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["canChat"],
+          "properties": {
+            "canChat": {
+              "type": "boolean"
+            },
+            "convo": {
+              "type": "ref",
+              "ref": "chat.bsky.convo.defs#convoView"
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/chat/bsky/convo/getConvoForMembers.json b/lexicons/chat/bsky/convo/getConvoForMembers.json
new file mode 100644
index 00000000..1d5d5b46
--- /dev/null
+++ b/lexicons/chat/bsky/convo/getConvoForMembers.json
@@ -0,0 +1,37 @@
+{
+  "lexicon": 1,
+  "id": "chat.bsky.convo.getConvoForMembers",
+  "defs": {
+    "main": {
+      "type": "query",
+      "parameters": {
+        "type": "params",
+        "required": ["members"],
+        "properties": {
+          "members": {
+            "type": "array",
+            "minLength": 1,
+            "maxLength": 10,
+            "items": {
+              "type": "string",
+              "format": "did"
+            }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["convo"],
+          "properties": {
+            "convo": {
+              "type": "ref",
+              "ref": "chat.bsky.convo.defs#convoView"
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/chat/bsky/convo/getLog.json b/lexicons/chat/bsky/convo/getLog.json
new file mode 100644
index 00000000..41aff351
--- /dev/null
+++ b/lexicons/chat/bsky/convo/getLog.json
@@ -0,0 +1,44 @@
+{
+  "lexicon": 1,
+  "id": "chat.bsky.convo.getLog",
+  "defs": {
+    "main": {
+      "type": "query",
+      "parameters": {
+        "type": "params",
+        "required": [],
+        "properties": {
+          "cursor": { "type": "string" }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["logs"],
+          "properties": {
+            "cursor": { "type": "string" },
+            "logs": {
+              "type": "array",
+              "items": {
+                "type": "union",
+                "refs": [
+                  "chat.bsky.convo.defs#logBeginConvo",
+                  "chat.bsky.convo.defs#logAcceptConvo",
+                  "chat.bsky.convo.defs#logLeaveConvo",
+                  "chat.bsky.convo.defs#logMuteConvo",
+                  "chat.bsky.convo.defs#logUnmuteConvo",
+                  "chat.bsky.convo.defs#logCreateMessage",
+                  "chat.bsky.convo.defs#logDeleteMessage",
+                  "chat.bsky.convo.defs#logReadMessage",
+                  "chat.bsky.convo.defs#logAddReaction",
+                  "chat.bsky.convo.defs#logRemoveReaction"
+                ]
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/chat/bsky/convo/getMessages.json b/lexicons/chat/bsky/convo/getMessages.json
new file mode 100644
index 00000000..628e744a
--- /dev/null
+++ b/lexicons/chat/bsky/convo/getMessages.json
@@ -0,0 +1,43 @@
+{
+  "lexicon": 1,
+  "id": "chat.bsky.convo.getMessages",
+  "defs": {
+    "main": {
+      "type": "query",
+      "parameters": {
+        "type": "params",
+        "required": ["convoId"],
+        "properties": {
+          "convoId": { "type": "string" },
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 100,
+            "default": 50
+          },
+          "cursor": { "type": "string" }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["messages"],
+          "properties": {
+            "cursor": { "type": "string" },
+            "messages": {
+              "type": "array",
+              "items": {
+                "type": "union",
+                "refs": [
+                  "chat.bsky.convo.defs#messageView",
+                  "chat.bsky.convo.defs#deletedMessageView"
+                ]
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/chat/bsky/convo/leaveConvo.json b/lexicons/chat/bsky/convo/leaveConvo.json
new file mode 100644
index 00000000..928e843f
--- /dev/null
+++ b/lexicons/chat/bsky/convo/leaveConvo.json
@@ -0,0 +1,30 @@
+{
+  "lexicon": 1,
+  "id": "chat.bsky.convo.leaveConvo",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["convoId"],
+          "properties": {
+            "convoId": { "type": "string" }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["convoId", "rev"],
+          "properties": {
+            "convoId": { "type": "string" },
+            "rev": { "type": "string" }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/chat/bsky/convo/listConvos.json b/lexicons/chat/bsky/convo/listConvos.json
new file mode 100644
index 00000000..27834ac7
--- /dev/null
+++ b/lexicons/chat/bsky/convo/listConvos.json
@@ -0,0 +1,46 @@
+{
+  "lexicon": 1,
+  "id": "chat.bsky.convo.listConvos",
+  "defs": {
+    "main": {
+      "type": "query",
+      "parameters": {
+        "type": "params",
+        "properties": {
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 100,
+            "default": 50
+          },
+          "cursor": { "type": "string" },
+          "readState": {
+            "type": "string",
+            "knownValues": ["unread"]
+          },
+          "status": {
+            "type": "string",
+            "knownValues": ["request", "accepted"]
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["convos"],
+          "properties": {
+            "cursor": { "type": "string" },
+            "convos": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "chat.bsky.convo.defs#convoView"
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/chat/bsky/convo/muteConvo.json b/lexicons/chat/bsky/convo/muteConvo.json
new file mode 100644
index 00000000..e63a9dbf
--- /dev/null
+++ b/lexicons/chat/bsky/convo/muteConvo.json
@@ -0,0 +1,32 @@
+{
+  "lexicon": 1,
+  "id": "chat.bsky.convo.muteConvo",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["convoId"],
+          "properties": {
+            "convoId": { "type": "string" }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["convo"],
+          "properties": {
+            "convo": {
+              "type": "ref",
+              "ref": "chat.bsky.convo.defs#convoView"
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/chat/bsky/convo/removeReaction.json b/lexicons/chat/bsky/convo/removeReaction.json
new file mode 100644
index 00000000..41867fb7
--- /dev/null
+++ b/lexicons/chat/bsky/convo/removeReaction.json
@@ -0,0 +1,51 @@
+{
+  "lexicon": 1,
+  "id": "chat.bsky.convo.removeReaction",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Removes an emoji reaction from a message. Requires authentication. It is idempotent, so multiple calls from the same user with the same emoji result in that reaction not being present, even if it already wasn't.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["convoId", "messageId", "value"],
+          "properties": {
+            "convoId": { "type": "string" },
+            "messageId": { "type": "string" },
+            "value": {
+              "type": "string",
+              "minLength": 1,
+              "maxLength": 64,
+              "minGraphemes": 1,
+              "maxGraphemes": 1
+            }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["message"],
+          "properties": {
+            "message": {
+              "type": "ref",
+              "ref": "chat.bsky.convo.defs#messageView"
+            }
+          }
+        }
+      },
+      "errors": [
+        {
+          "name": "ReactionMessageDeleted",
+          "description": "Indicates that the message has been deleted and reactions can no longer be added/removed."
+        },
+        {
+          "name": "ReactionInvalidValue",
+          "description": "Indicates the value for the reaction is not acceptable. In general, this means it is not an emoji."
+        }
+      ]
+    }
+  }
+}
diff --git a/lexicons/chat/bsky/convo/sendMessage.json b/lexicons/chat/bsky/convo/sendMessage.json
new file mode 100644
index 00000000..736c1abd
--- /dev/null
+++ b/lexicons/chat/bsky/convo/sendMessage.json
@@ -0,0 +1,30 @@
+{
+  "lexicon": 1,
+  "id": "chat.bsky.convo.sendMessage",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["convoId", "message"],
+          "properties": {
+            "convoId": { "type": "string" },
+            "message": {
+              "type": "ref",
+              "ref": "chat.bsky.convo.defs#messageInput"
+            }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "ref",
+          "ref": "chat.bsky.convo.defs#messageView"
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/chat/bsky/convo/sendMessageBatch.json b/lexicons/chat/bsky/convo/sendMessageBatch.json
new file mode 100644
index 00000000..d867a66d
--- /dev/null
+++ b/lexicons/chat/bsky/convo/sendMessageBatch.json
@@ -0,0 +1,53 @@
+{
+  "lexicon": 1,
+  "id": "chat.bsky.convo.sendMessageBatch",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["items"],
+          "properties": {
+            "items": {
+              "type": "array",
+              "maxLength": 100,
+              "items": {
+                "type": "ref",
+                "ref": "#batchItem"
+              }
+            }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["items"],
+          "properties": {
+            "items": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "chat.bsky.convo.defs#messageView"
+              }
+            }
+          }
+        }
+      }
+    },
+    "batchItem": {
+      "type": "object",
+      "required": ["convoId", "message"],
+      "properties": {
+        "convoId": { "type": "string" },
+        "message": {
+          "type": "ref",
+          "ref": "chat.bsky.convo.defs#messageInput"
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/chat/bsky/convo/unmuteConvo.json b/lexicons/chat/bsky/convo/unmuteConvo.json
new file mode 100644
index 00000000..7f84dac6
--- /dev/null
+++ b/lexicons/chat/bsky/convo/unmuteConvo.json
@@ -0,0 +1,32 @@
+{
+  "lexicon": 1,
+  "id": "chat.bsky.convo.unmuteConvo",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["convoId"],
+          "properties": {
+            "convoId": { "type": "string" }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["convo"],
+          "properties": {
+            "convo": {
+              "type": "ref",
+              "ref": "chat.bsky.convo.defs#convoView"
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/chat/bsky/convo/updateAllRead.json b/lexicons/chat/bsky/convo/updateAllRead.json
new file mode 100644
index 00000000..eab4b29e
--- /dev/null
+++ b/lexicons/chat/bsky/convo/updateAllRead.json
@@ -0,0 +1,34 @@
+{
+  "lexicon": 1,
+  "id": "chat.bsky.convo.updateAllRead",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "properties": {
+            "status": {
+              "type": "string",
+              "knownValues": ["request", "accepted"]
+            }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["updatedCount"],
+          "properties": {
+            "updatedCount": {
+              "description": "The count of updated convos.",
+              "type": "integer"
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/chat/bsky/convo/updateRead.json b/lexicons/chat/bsky/convo/updateRead.json
new file mode 100644
index 00000000..525c147e
--- /dev/null
+++ b/lexicons/chat/bsky/convo/updateRead.json
@@ -0,0 +1,33 @@
+{
+  "lexicon": 1,
+  "id": "chat.bsky.convo.updateRead",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["convoId"],
+          "properties": {
+            "convoId": { "type": "string" },
+            "messageId": { "type": "string" }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["convo"],
+          "properties": {
+            "convo": {
+              "type": "ref",
+              "ref": "chat.bsky.convo.defs#convoView"
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/chat/bsky/moderation/getActorMetadata.json b/lexicons/chat/bsky/moderation/getActorMetadata.json
new file mode 100644
index 00000000..eb35bab9
--- /dev/null
+++ b/lexicons/chat/bsky/moderation/getActorMetadata.json
@@ -0,0 +1,43 @@
+{
+  "lexicon": 1,
+  "id": "chat.bsky.moderation.getActorMetadata",
+  "defs": {
+    "main": {
+      "type": "query",
+      "parameters": {
+        "type": "params",
+        "required": ["actor"],
+        "properties": {
+          "actor": { "type": "string", "format": "did" }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["day", "month", "all"],
+          "properties": {
+            "day": { "type": "ref", "ref": "#metadata" },
+            "month": { "type": "ref", "ref": "#metadata" },
+            "all": { "type": "ref", "ref": "#metadata" }
+          }
+        }
+      }
+    },
+    "metadata": {
+      "type": "object",
+      "required": [
+        "messagesSent",
+        "messagesReceived",
+        "convos",
+        "convosStarted"
+      ],
+      "properties": {
+        "messagesSent": { "type": "integer" },
+        "messagesReceived": { "type": "integer" },
+        "convos": { "type": "integer" },
+        "convosStarted": { "type": "integer" }
+      }
+    }
+  }
+}
diff --git a/lexicons/chat/bsky/moderation/getMessageContext.json b/lexicons/chat/bsky/moderation/getMessageContext.json
new file mode 100644
index 00000000..df08af82
--- /dev/null
+++ b/lexicons/chat/bsky/moderation/getMessageContext.json
@@ -0,0 +1,41 @@
+{
+  "lexicon": 1,
+  "id": "chat.bsky.moderation.getMessageContext",
+  "defs": {
+    "main": {
+      "type": "query",
+      "parameters": {
+        "type": "params",
+        "required": ["messageId"],
+        "properties": {
+          "convoId": {
+            "type": "string",
+            "description": "Conversation that the message is from. NOTE: this field will eventually be required."
+          },
+          "messageId": { "type": "string" },
+          "before": { "type": "integer", "default": 5 },
+          "after": { "type": "integer", "default": 5 }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["messages"],
+          "properties": {
+            "messages": {
+              "type": "array",
+              "items": {
+                "type": "union",
+                "refs": [
+                  "chat.bsky.convo.defs#messageView",
+                  "chat.bsky.convo.defs#deletedMessageView"
+                ]
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/chat/bsky/moderation/updateActorAccess.json b/lexicons/chat/bsky/moderation/updateActorAccess.json
new file mode 100644
index 00000000..56c84a74
--- /dev/null
+++ b/lexicons/chat/bsky/moderation/updateActorAccess.json
@@ -0,0 +1,21 @@
+{
+  "lexicon": 1,
+  "id": "chat.bsky.moderation.updateActorAccess",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["actor", "allowAccess"],
+          "properties": {
+            "actor": { "type": "string", "format": "did" },
+            "allowAccess": { "type": "boolean" },
+            "ref": { "type": "string" }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/com/atproto/admin/defs.json b/lexicons/com/atproto/admin/defs.json
new file mode 100644
index 00000000..24dfd030
--- /dev/null
+++ b/lexicons/com/atproto/admin/defs.json
@@ -0,0 +1,71 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.admin.defs",
+  "defs": {
+    "statusAttr": {
+      "type": "object",
+      "required": ["applied"],
+      "properties": {
+        "applied": { "type": "boolean" },
+        "ref": { "type": "string" }
+      }
+    },
+    "accountView": {
+      "type": "object",
+      "required": ["did", "handle", "indexedAt"],
+      "properties": {
+        "did": { "type": "string", "format": "did" },
+        "handle": { "type": "string", "format": "handle" },
+        "email": { "type": "string" },
+        "relatedRecords": { "type": "array", "items": { "type": "unknown" } },
+        "indexedAt": { "type": "string", "format": "datetime" },
+        "invitedBy": {
+          "type": "ref",
+          "ref": "com.atproto.server.defs#inviteCode"
+        },
+        "invites": {
+          "type": "array",
+          "items": {
+            "type": "ref",
+            "ref": "com.atproto.server.defs#inviteCode"
+          }
+        },
+        "invitesDisabled": { "type": "boolean" },
+        "emailConfirmedAt": { "type": "string", "format": "datetime" },
+        "inviteNote": { "type": "string" },
+        "deactivatedAt": { "type": "string", "format": "datetime" },
+        "threatSignatures": {
+          "type": "array",
+          "items": {
+            "type": "ref",
+            "ref": "#threatSignature"
+          }
+        }
+      }
+    },
+    "repoRef": {
+      "type": "object",
+      "required": ["did"],
+      "properties": {
+        "did": { "type": "string", "format": "did" }
+      }
+    },
+    "repoBlobRef": {
+      "type": "object",
+      "required": ["did", "cid"],
+      "properties": {
+        "did": { "type": "string", "format": "did" },
+        "cid": { "type": "string", "format": "cid" },
+        "recordUri": { "type": "string", "format": "at-uri" }
+      }
+    },
+    "threatSignature": {
+      "type": "object",
+      "required": ["property", "value"],
+      "properties": {
+        "property": { "type": "string" },
+        "value": { "type": "string" }
+      }
+    }
+  }
+}
diff --git a/lexicons/com/atproto/admin/deleteAccount.json b/lexicons/com/atproto/admin/deleteAccount.json
new file mode 100644
index 00000000..bd7532ce
--- /dev/null
+++ b/lexicons/com/atproto/admin/deleteAccount.json
@@ -0,0 +1,20 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.admin.deleteAccount",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Delete a user account as an administrator.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["did"],
+          "properties": {
+            "did": { "type": "string", "format": "did" }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/com/atproto/admin/disableAccountInvites.json b/lexicons/com/atproto/admin/disableAccountInvites.json
new file mode 100644
index 00000000..8bd6cabb
--- /dev/null
+++ b/lexicons/com/atproto/admin/disableAccountInvites.json
@@ -0,0 +1,24 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.admin.disableAccountInvites",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Disable an account from receiving new invite codes, but does not invalidate existing codes.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["account"],
+          "properties": {
+            "account": { "type": "string", "format": "did" },
+            "note": {
+              "type": "string",
+              "description": "Optional reason for disabled invites."
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/com/atproto/admin/disableInviteCodes.json b/lexicons/com/atproto/admin/disableInviteCodes.json
new file mode 100644
index 00000000..914c8cb2
--- /dev/null
+++ b/lexicons/com/atproto/admin/disableInviteCodes.json
@@ -0,0 +1,26 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.admin.disableInviteCodes",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Disable some set of codes and/or all codes associated with a set of users.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "properties": {
+            "codes": {
+              "type": "array",
+              "items": { "type": "string" }
+            },
+            "accounts": {
+              "type": "array",
+              "items": { "type": "string" }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/com/atproto/admin/enableAccountInvites.json b/lexicons/com/atproto/admin/enableAccountInvites.json
new file mode 100644
index 00000000..97443cd9
--- /dev/null
+++ b/lexicons/com/atproto/admin/enableAccountInvites.json
@@ -0,0 +1,24 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.admin.enableAccountInvites",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Re-enable an account's ability to receive invite codes.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["account"],
+          "properties": {
+            "account": { "type": "string", "format": "did" },
+            "note": {
+              "type": "string",
+              "description": "Optional reason for enabled invites."
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/com/atproto/admin/getAccountInfo.json b/lexicons/com/atproto/admin/getAccountInfo.json
new file mode 100644
index 00000000..e5d33fdc
--- /dev/null
+++ b/lexicons/com/atproto/admin/getAccountInfo.json
@@ -0,0 +1,24 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.admin.getAccountInfo",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get details about an account.",
+      "parameters": {
+        "type": "params",
+        "required": ["did"],
+        "properties": {
+          "did": { "type": "string", "format": "did" }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "ref",
+          "ref": "com.atproto.admin.defs#accountView"
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/com/atproto/admin/getAccountInfos.json b/lexicons/com/atproto/admin/getAccountInfos.json
new file mode 100644
index 00000000..45d97e08
--- /dev/null
+++ b/lexicons/com/atproto/admin/getAccountInfos.json
@@ -0,0 +1,36 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.admin.getAccountInfos",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get details about some accounts.",
+      "parameters": {
+        "type": "params",
+        "required": ["dids"],
+        "properties": {
+          "dids": {
+            "type": "array",
+            "items": { "type": "string", "format": "did" }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["infos"],
+          "properties": {
+            "infos": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "com.atproto.admin.defs#accountView"
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/com/atproto/admin/getInviteCodes.json b/lexicons/com/atproto/admin/getInviteCodes.json
new file mode 100644
index 00000000..883604dc
--- /dev/null
+++ b/lexicons/com/atproto/admin/getInviteCodes.json
@@ -0,0 +1,44 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.admin.getInviteCodes",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get an admin view of invite codes.",
+      "parameters": {
+        "type": "params",
+        "properties": {
+          "sort": {
+            "type": "string",
+            "knownValues": ["recent", "usage"],
+            "default": "recent"
+          },
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 500,
+            "default": 100
+          },
+          "cursor": { "type": "string" }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["codes"],
+          "properties": {
+            "cursor": { "type": "string" },
+            "codes": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "com.atproto.server.defs#inviteCode"
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/com/atproto/admin/getSubjectStatus.json b/lexicons/com/atproto/admin/getSubjectStatus.json
new file mode 100644
index 00000000..634e140f
--- /dev/null
+++ b/lexicons/com/atproto/admin/getSubjectStatus.json
@@ -0,0 +1,43 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.admin.getSubjectStatus",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get the service-specific admin status of a subject (account, record, or blob).",
+      "parameters": {
+        "type": "params",
+        "properties": {
+          "did": { "type": "string", "format": "did" },
+          "uri": { "type": "string", "format": "at-uri" },
+          "blob": { "type": "string", "format": "cid" }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["subject"],
+          "properties": {
+            "subject": {
+              "type": "union",
+              "refs": [
+                "com.atproto.admin.defs#repoRef",
+                "com.atproto.repo.strongRef",
+                "com.atproto.admin.defs#repoBlobRef"
+              ]
+            },
+            "takedown": {
+              "type": "ref",
+              "ref": "com.atproto.admin.defs#statusAttr"
+            },
+            "deactivated": {
+              "type": "ref",
+              "ref": "com.atproto.admin.defs#statusAttr"
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/com/atproto/admin/searchAccounts.json b/lexicons/com/atproto/admin/searchAccounts.json
new file mode 100644
index 00000000..5add85d6
--- /dev/null
+++ b/lexicons/com/atproto/admin/searchAccounts.json
@@ -0,0 +1,40 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.admin.searchAccounts",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get list of accounts that matches your search query.",
+      "parameters": {
+        "type": "params",
+        "properties": {
+          "email": { "type": "string" },
+          "cursor": { "type": "string" },
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 100,
+            "default": 50
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["accounts"],
+          "properties": {
+            "cursor": { "type": "string" },
+            "accounts": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "com.atproto.admin.defs#accountView"
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/com/atproto/admin/sendEmail.json b/lexicons/com/atproto/admin/sendEmail.json
new file mode 100644
index 00000000..4c33dcd8
--- /dev/null
+++ b/lexicons/com/atproto/admin/sendEmail.json
@@ -0,0 +1,37 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.admin.sendEmail",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Send email to a user's account email address.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["recipientDid", "content", "senderDid"],
+          "properties": {
+            "recipientDid": { "type": "string", "format": "did" },
+            "content": { "type": "string" },
+            "subject": { "type": "string" },
+            "senderDid": { "type": "string", "format": "did" },
+            "comment": {
+              "type": "string",
+              "description": "Additional comment by the sender that won't be used in the email itself but helpful to provide more context for moderators/reviewers"
+            }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["sent"],
+          "properties": {
+            "sent": { "type": "boolean" }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/com/atproto/admin/updateAccountEmail.json b/lexicons/com/atproto/admin/updateAccountEmail.json
new file mode 100644
index 00000000..f0c3eeb7
--- /dev/null
+++ b/lexicons/com/atproto/admin/updateAccountEmail.json
@@ -0,0 +1,25 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.admin.updateAccountEmail",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Administrative action to update an account's email.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["account", "email"],
+          "properties": {
+            "account": {
+              "type": "string",
+              "format": "at-identifier",
+              "description": "The handle or DID of the repo."
+            },
+            "email": { "type": "string" }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/com/atproto/admin/updateAccountHandle.json b/lexicons/com/atproto/admin/updateAccountHandle.json
new file mode 100644
index 00000000..23b86566
--- /dev/null
+++ b/lexicons/com/atproto/admin/updateAccountHandle.json
@@ -0,0 +1,21 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.admin.updateAccountHandle",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Administrative action to update an account's handle.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["did", "handle"],
+          "properties": {
+            "did": { "type": "string", "format": "did" },
+            "handle": { "type": "string", "format": "handle" }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/com/atproto/admin/updateAccountPassword.json b/lexicons/com/atproto/admin/updateAccountPassword.json
new file mode 100644
index 00000000..76c69fec
--- /dev/null
+++ b/lexicons/com/atproto/admin/updateAccountPassword.json
@@ -0,0 +1,21 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.admin.updateAccountPassword",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Update the password for a user account as an administrator.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["did", "password"],
+          "properties": {
+            "did": { "type": "string", "format": "did" },
+            "password": { "type": "string" }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/com/atproto/admin/updateAccountSigningKey.json b/lexicons/com/atproto/admin/updateAccountSigningKey.json
new file mode 100644
index 00000000..c8289a02
--- /dev/null
+++ b/lexicons/com/atproto/admin/updateAccountSigningKey.json
@@ -0,0 +1,25 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.admin.updateAccountSigningKey",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Administrative action to update an account's signing key in their Did document.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["did", "signingKey"],
+          "properties": {
+            "did": { "type": "string", "format": "did" },
+            "signingKey": {
+              "type": "string",
+              "format": "did",
+              "description": "Did-key formatted public key"
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/com/atproto/admin/updateSubjectStatus.json b/lexicons/com/atproto/admin/updateSubjectStatus.json
new file mode 100644
index 00000000..799dee7b
--- /dev/null
+++ b/lexicons/com/atproto/admin/updateSubjectStatus.json
@@ -0,0 +1,56 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.admin.updateSubjectStatus",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Update the service-specific admin status of a subject (account, record, or blob).",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["subject"],
+          "properties": {
+            "subject": {
+              "type": "union",
+              "refs": [
+                "com.atproto.admin.defs#repoRef",
+                "com.atproto.repo.strongRef",
+                "com.atproto.admin.defs#repoBlobRef"
+              ]
+            },
+            "takedown": {
+              "type": "ref",
+              "ref": "com.atproto.admin.defs#statusAttr"
+            },
+            "deactivated": {
+              "type": "ref",
+              "ref": "com.atproto.admin.defs#statusAttr"
+            }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["subject"],
+          "properties": {
+            "subject": {
+              "type": "union",
+              "refs": [
+                "com.atproto.admin.defs#repoRef",
+                "com.atproto.repo.strongRef",
+                "com.atproto.admin.defs#repoBlobRef"
+              ]
+            },
+            "takedown": {
+              "type": "ref",
+              "ref": "com.atproto.admin.defs#statusAttr"
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/com/atproto/identity/defs.json b/lexicons/com/atproto/identity/defs.json
new file mode 100644
index 00000000..9449cdd9
--- /dev/null
+++ b/lexicons/com/atproto/identity/defs.json
@@ -0,0 +1,22 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.identity.defs",
+  "defs": {
+    "identityInfo": {
+      "type": "object",
+      "required": ["did", "handle", "didDoc"],
+      "properties": {
+        "did": { "type": "string", "format": "did" },
+        "handle": {
+          "type": "string",
+          "format": "handle",
+          "description": "The validated handle of the account; or 'handle.invalid' if the handle did not bi-directionally match the DID document."
+        },
+        "didDoc": {
+          "type": "unknown",
+          "description": "The complete DID document for the identity."
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/com/atproto/identity/getRecommendedDidCredentials.json b/lexicons/com/atproto/identity/getRecommendedDidCredentials.json
new file mode 100644
index 00000000..3506dbec
--- /dev/null
+++ b/lexicons/com/atproto/identity/getRecommendedDidCredentials.json
@@ -0,0 +1,29 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.identity.getRecommendedDidCredentials",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Describe the credentials that should be included in the DID doc of an account that is migrating to this service.",
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "properties": {
+            "rotationKeys": {
+              "description": "Recommended rotation keys for PLC dids. Should be undefined (or ignored) for did:webs.",
+              "type": "array",
+              "items": { "type": "string" }
+            },
+            "alsoKnownAs": {
+              "type": "array",
+              "items": { "type": "string" }
+            },
+            "verificationMethods": { "type": "unknown" },
+            "services": { "type": "unknown" }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/com/atproto/identity/refreshIdentity.json b/lexicons/com/atproto/identity/refreshIdentity.json
new file mode 100644
index 00000000..4c56084b
--- /dev/null
+++ b/lexicons/com/atproto/identity/refreshIdentity.json
@@ -0,0 +1,44 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.identity.refreshIdentity",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Request that the server re-resolve an identity (DID and handle). The server may ignore this request, or require authentication, depending on the role, implementation, and policy of the server.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["identifier"],
+          "properties": {
+            "identifier": {
+              "type": "string",
+              "format": "at-identifier"
+            }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "ref",
+          "ref": "com.atproto.identity.defs#identityInfo"
+        }
+      },
+      "errors": [
+        {
+          "name": "HandleNotFound",
+          "description": "The resolution process confirmed that the handle does not resolve to any DID."
+        },
+        {
+          "name": "DidNotFound",
+          "description": "The DID resolution process confirmed that there is no current DID."
+        },
+        {
+          "name": "DidDeactivated",
+          "description": "The DID previously existed, but has been deactivated."
+        }
+      ]
+    }
+  }
+}
diff --git a/lexicons/com/atproto/identity/requestPlcOperationSignature.json b/lexicons/com/atproto/identity/requestPlcOperationSignature.json
new file mode 100644
index 00000000..4aa8b18f
--- /dev/null
+++ b/lexicons/com/atproto/identity/requestPlcOperationSignature.json
@@ -0,0 +1,10 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.identity.requestPlcOperationSignature",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Request an email with a code to in order to request a signed PLC operation. Requires Auth."
+    }
+  }
+}
diff --git a/lexicons/com/atproto/identity/resolveDid.json b/lexicons/com/atproto/identity/resolveDid.json
new file mode 100644
index 00000000..24c129a3
--- /dev/null
+++ b/lexicons/com/atproto/identity/resolveDid.json
@@ -0,0 +1,44 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.identity.resolveDid",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Resolves DID to DID document. Does not bi-directionally verify handle.",
+      "parameters": {
+        "type": "params",
+        "required": ["did"],
+        "properties": {
+          "did": {
+            "type": "string",
+            "format": "did",
+            "description": "DID to resolve."
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["didDoc"],
+          "properties": {
+            "didDoc": {
+              "type": "unknown",
+              "description": "The complete DID document for the identity."
+            }
+          }
+        }
+      },
+      "errors": [
+        {
+          "name": "DidNotFound",
+          "description": "The DID resolution process confirmed that there is no current DID."
+        },
+        {
+          "name": "DidDeactivated",
+          "description": "The DID previously existed, but has been deactivated."
+        }
+      ]
+    }
+  }
+}
diff --git a/lexicons/com/atproto/identity/resolveHandle.json b/lexicons/com/atproto/identity/resolveHandle.json
new file mode 100644
index 00000000..69751a52
--- /dev/null
+++ b/lexicons/com/atproto/identity/resolveHandle.json
@@ -0,0 +1,37 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.identity.resolveHandle",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Resolves an atproto handle (hostname) to a DID. Does not necessarily bi-directionally verify against the the DID document.",
+      "parameters": {
+        "type": "params",
+        "required": ["handle"],
+        "properties": {
+          "handle": {
+            "type": "string",
+            "format": "handle",
+            "description": "The handle to resolve."
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["did"],
+          "properties": {
+            "did": { "type": "string", "format": "did" }
+          }
+        }
+      },
+      "errors": [
+        {
+          "name": "HandleNotFound",
+          "description": "The resolution process confirmed that the handle does not resolve to any DID."
+        }
+      ]
+    }
+  }
+}
diff --git a/lexicons/com/atproto/identity/resolveIdentity.json b/lexicons/com/atproto/identity/resolveIdentity.json
new file mode 100644
index 00000000..fe33441c
--- /dev/null
+++ b/lexicons/com/atproto/identity/resolveIdentity.json
@@ -0,0 +1,42 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.identity.resolveIdentity",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Resolves an identity (DID or Handle) to a full identity (DID document and verified handle).",
+      "parameters": {
+        "type": "params",
+        "required": ["identifier"],
+        "properties": {
+          "identifier": {
+            "type": "string",
+            "format": "at-identifier",
+            "description": "Handle or DID to resolve."
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "ref",
+          "ref": "com.atproto.identity.defs#identityInfo"
+        }
+      },
+      "errors": [
+        {
+          "name": "HandleNotFound",
+          "description": "The resolution process confirmed that the handle does not resolve to any DID."
+        },
+        {
+          "name": "DidNotFound",
+          "description": "The DID resolution process confirmed that there is no current DID."
+        },
+        {
+          "name": "DidDeactivated",
+          "description": "The DID previously existed, but has been deactivated."
+        }
+      ]
+    }
+  }
+}
diff --git a/lexicons/com/atproto/identity/signPlcOperation.json b/lexicons/com/atproto/identity/signPlcOperation.json
new file mode 100644
index 00000000..05a952ca
--- /dev/null
+++ b/lexicons/com/atproto/identity/signPlcOperation.json
@@ -0,0 +1,45 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.identity.signPlcOperation",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Signs a PLC operation to update some value(s) in the requesting DID's document.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "properties": {
+            "token": {
+              "description": "A token received through com.atproto.identity.requestPlcOperationSignature",
+              "type": "string"
+            },
+            "rotationKeys": {
+              "type": "array",
+              "items": { "type": "string" }
+            },
+            "alsoKnownAs": {
+              "type": "array",
+              "items": { "type": "string" }
+            },
+            "verificationMethods": { "type": "unknown" },
+            "services": { "type": "unknown" }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["operation"],
+          "properties": {
+            "operation": {
+              "type": "unknown",
+              "description": "A signed DID PLC operation."
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/com/atproto/identity/submitPlcOperation.json b/lexicons/com/atproto/identity/submitPlcOperation.json
new file mode 100644
index 00000000..280f5003
--- /dev/null
+++ b/lexicons/com/atproto/identity/submitPlcOperation.json
@@ -0,0 +1,20 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.identity.submitPlcOperation",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Validates a PLC operation to ensure that it doesn't violate a service's constraints or get the identity into a bad state, then submits it to the PLC registry",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["operation"],
+          "properties": {
+            "operation": { "type": "unknown" }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/com/atproto/identity/updateHandle.json b/lexicons/com/atproto/identity/updateHandle.json
new file mode 100644
index 00000000..9bb5c347
--- /dev/null
+++ b/lexicons/com/atproto/identity/updateHandle.json
@@ -0,0 +1,24 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.identity.updateHandle",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Updates the current account's handle. Verifies handle validity, and updates did:plc document if necessary. Implemented by PDS, and requires auth.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["handle"],
+          "properties": {
+            "handle": {
+              "type": "string",
+              "format": "handle",
+              "description": "The new handle."
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/com/atproto/label/defs.json b/lexicons/com/atproto/label/defs.json
new file mode 100644
index 00000000..6f4c1ab9
--- /dev/null
+++ b/lexicons/com/atproto/label/defs.json
@@ -0,0 +1,156 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.label.defs",
+  "defs": {
+    "label": {
+      "type": "object",
+      "description": "Metadata tag on an atproto resource (eg, repo or record).",
+      "required": ["src", "uri", "val", "cts"],
+      "properties": {
+        "ver": {
+          "type": "integer",
+          "description": "The AT Protocol version of the label object."
+        },
+        "src": {
+          "type": "string",
+          "format": "did",
+          "description": "DID of the actor who created this label."
+        },
+        "uri": {
+          "type": "string",
+          "format": "uri",
+          "description": "AT URI of the record, repository (account), or other resource that this label applies to."
+        },
+        "cid": {
+          "type": "string",
+          "format": "cid",
+          "description": "Optionally, CID specifying the specific version of 'uri' resource this label applies to."
+        },
+        "val": {
+          "type": "string",
+          "maxLength": 128,
+          "description": "The short string name of the value or type of this label."
+        },
+        "neg": {
+          "type": "boolean",
+          "description": "If true, this is a negation label, overwriting a previous label."
+        },
+        "cts": {
+          "type": "string",
+          "format": "datetime",
+          "description": "Timestamp when this label was created."
+        },
+        "exp": {
+          "type": "string",
+          "format": "datetime",
+          "description": "Timestamp at which this label expires (no longer applies)."
+        },
+        "sig": {
+          "type": "bytes",
+          "description": "Signature of dag-cbor encoded label."
+        }
+      }
+    },
+    "selfLabels": {
+      "type": "object",
+      "description": "Metadata tags on an atproto record, published by the author within the record.",
+      "required": ["values"],
+      "properties": {
+        "values": {
+          "type": "array",
+          "items": { "type": "ref", "ref": "#selfLabel" },
+          "maxLength": 10
+        }
+      }
+    },
+    "selfLabel": {
+      "type": "object",
+      "description": "Metadata tag on an atproto record, published by the author within the record. Note that schemas should use #selfLabels, not #selfLabel.",
+      "required": ["val"],
+      "properties": {
+        "val": {
+          "type": "string",
+          "maxLength": 128,
+          "description": "The short string name of the value or type of this label."
+        }
+      }
+    },
+    "labelValueDefinition": {
+      "type": "object",
+      "description": "Declares a label value and its expected interpretations and behaviors.",
+      "required": ["identifier", "severity", "blurs", "locales"],
+      "properties": {
+        "identifier": {
+          "type": "string",
+          "description": "The value of the label being defined. Must only include lowercase ascii and the '-' character ([a-z-]+).",
+          "maxLength": 100,
+          "maxGraphemes": 100
+        },
+        "severity": {
+          "type": "string",
+          "description": "How should a client visually convey this label? 'inform' means neutral and informational; 'alert' means negative and warning; 'none' means show nothing.",
+          "knownValues": ["inform", "alert", "none"]
+        },
+        "blurs": {
+          "type": "string",
+          "description": "What should this label hide in the UI, if applied? 'content' hides all of the target; 'media' hides the images/video/audio; 'none' hides nothing.",
+          "knownValues": ["content", "media", "none"]
+        },
+        "defaultSetting": {
+          "type": "string",
+          "description": "The default setting for this label.",
+          "knownValues": ["ignore", "warn", "hide"],
+          "default": "warn"
+        },
+        "adultOnly": {
+          "type": "boolean",
+          "description": "Does the user need to have adult content enabled in order to configure this label?"
+        },
+        "locales": {
+          "type": "array",
+          "items": { "type": "ref", "ref": "#labelValueDefinitionStrings" }
+        }
+      }
+    },
+    "labelValueDefinitionStrings": {
+      "type": "object",
+      "description": "Strings which describe the label in the UI, localized into a specific language.",
+      "required": ["lang", "name", "description"],
+      "properties": {
+        "lang": {
+          "type": "string",
+          "description": "The code of the language these strings are written in.",
+          "format": "language"
+        },
+        "name": {
+          "type": "string",
+          "description": "A short human-readable name for the label.",
+          "maxGraphemes": 64,
+          "maxLength": 640
+        },
+        "description": {
+          "type": "string",
+          "description": "A longer description of what the label means and why it might be applied.",
+          "maxGraphemes": 10000,
+          "maxLength": 100000
+        }
+      }
+    },
+    "labelValue": {
+      "type": "string",
+      "knownValues": [
+        "!hide",
+        "!no-promote",
+        "!warn",
+        "!no-unauthenticated",
+        "dmca-violation",
+        "doxxing",
+        "porn",
+        "sexual",
+        "nudity",
+        "nsfl",
+        "gore"
+      ]
+    }
+  }
+}
diff --git a/lexicons/com/atproto/label/queryLabels.json b/lexicons/com/atproto/label/queryLabels.json
new file mode 100644
index 00000000..6c81cb0b
--- /dev/null
+++ b/lexicons/com/atproto/label/queryLabels.json
@@ -0,0 +1,47 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.label.queryLabels",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Find labels relevant to the provided AT-URI patterns. Public endpoint for moderation services, though may return different or additional results with auth.",
+      "parameters": {
+        "type": "params",
+        "required": ["uriPatterns"],
+        "properties": {
+          "uriPatterns": {
+            "type": "array",
+            "items": { "type": "string" },
+            "description": "List of AT URI patterns to match (boolean 'OR'). Each may be a prefix (ending with '*'; will match inclusive of the string leading to '*'), or a full URI."
+          },
+          "sources": {
+            "type": "array",
+            "items": { "type": "string", "format": "did" },
+            "description": "Optional list of label sources (DIDs) to filter on."
+          },
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 250,
+            "default": 50
+          },
+          "cursor": { "type": "string" }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["labels"],
+          "properties": {
+            "cursor": { "type": "string" },
+            "labels": {
+              "type": "array",
+              "items": { "type": "ref", "ref": "com.atproto.label.defs#label" }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/com/atproto/label/subscribeLabels.json b/lexicons/com/atproto/label/subscribeLabels.json
new file mode 100644
index 00000000..5fb1a852
--- /dev/null
+++ b/lexicons/com/atproto/label/subscribeLabels.json
@@ -0,0 +1,50 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.label.subscribeLabels",
+  "defs": {
+    "main": {
+      "type": "subscription",
+      "description": "Subscribe to stream of labels (and negations). Public endpoint implemented by mod services. Uses same sequencing scheme as repo event stream.",
+      "parameters": {
+        "type": "params",
+        "properties": {
+          "cursor": {
+            "type": "integer",
+            "description": "The last known event seq number to backfill from."
+          }
+        }
+      },
+      "message": {
+        "schema": {
+          "type": "union",
+          "refs": ["#labels", "#info"]
+        }
+      },
+      "errors": [{ "name": "FutureCursor" }]
+    },
+    "labels": {
+      "type": "object",
+      "required": ["seq", "labels"],
+      "properties": {
+        "seq": { "type": "integer" },
+        "labels": {
+          "type": "array",
+          "items": { "type": "ref", "ref": "com.atproto.label.defs#label" }
+        }
+      }
+    },
+    "info": {
+      "type": "object",
+      "required": ["name"],
+      "properties": {
+        "name": {
+          "type": "string",
+          "knownValues": ["OutdatedCursor"]
+        },
+        "message": {
+          "type": "string"
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/com/atproto/lexicon/schema.json b/lexicons/com/atproto/lexicon/schema.json
new file mode 100644
index 00000000..e79337ba
--- /dev/null
+++ b/lexicons/com/atproto/lexicon/schema.json
@@ -0,0 +1,21 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.lexicon.schema",
+  "defs": {
+    "main": {
+      "type": "record",
+      "description": "Representation of Lexicon schemas themselves, when published as atproto records. Note that the schema language is not defined in Lexicon; this meta schema currently only includes a single version field ('lexicon'). See the atproto specifications for description of the other expected top-level fields ('id', 'defs', etc).",
+      "key": "nsid",
+      "record": {
+        "type": "object",
+        "required": ["lexicon"],
+        "properties": {
+          "lexicon": {
+            "type": "integer",
+            "description": "Indicates the 'version' of the Lexicon language. Must be '1' for the current atproto/Lexicon schema system."
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/com/atproto/moderation/createReport.json b/lexicons/com/atproto/moderation/createReport.json
new file mode 100644
index 00000000..2b8fc740
--- /dev/null
+++ b/lexicons/com/atproto/moderation/createReport.json
@@ -0,0 +1,90 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.moderation.createReport",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Submit a moderation report regarding an atproto account or record. Implemented by moderation services (with PDS proxying), and requires auth.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["reasonType", "subject"],
+          "properties": {
+            "reasonType": {
+              "type": "ref",
+              "description": "Indicates the broad category of violation the report is for.",
+              "ref": "com.atproto.moderation.defs#reasonType"
+            },
+            "reason": {
+              "type": "string",
+              "maxGraphemes": 2000,
+              "maxLength": 20000,
+              "description": "Additional context about the content and violation."
+            },
+            "subject": {
+              "type": "union",
+              "refs": [
+                "com.atproto.admin.defs#repoRef",
+                "com.atproto.repo.strongRef"
+              ]
+            },
+            "modTool": {
+              "type": "ref",
+              "ref": "#modTool"
+            }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": [
+            "id",
+            "reasonType",
+            "subject",
+            "reportedBy",
+            "createdAt"
+          ],
+          "properties": {
+            "id": { "type": "integer" },
+            "reasonType": {
+              "type": "ref",
+              "ref": "com.atproto.moderation.defs#reasonType"
+            },
+            "reason": {
+              "type": "string",
+              "maxGraphemes": 2000,
+              "maxLength": 20000
+            },
+            "subject": {
+              "type": "union",
+              "refs": [
+                "com.atproto.admin.defs#repoRef",
+                "com.atproto.repo.strongRef"
+              ]
+            },
+            "reportedBy": { "type": "string", "format": "did" },
+            "createdAt": { "type": "string", "format": "datetime" }
+          }
+        }
+      }
+    },
+    "modTool": {
+      "type": "object",
+      "description": "Moderation tool information for tracing the source of the action",
+      "required": ["name"],
+      "properties": {
+        "name": {
+          "type": "string",
+          "description": "Name/identifier of the source (e.g., 'bsky-app/android', 'bsky-web/chrome')"
+        },
+        "meta": {
+          "type": "unknown",
+          "description": "Additional arbitrary metadata about the source"
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/com/atproto/moderation/defs.json b/lexicons/com/atproto/moderation/defs.json
new file mode 100644
index 00000000..cc946dc6
--- /dev/null
+++ b/lexicons/com/atproto/moderation/defs.json
@@ -0,0 +1,99 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.moderation.defs",
+  "defs": {
+    "reasonType": {
+      "type": "string",
+      "knownValues": [
+        "com.atproto.moderation.defs#reasonSpam",
+        "com.atproto.moderation.defs#reasonViolation",
+        "com.atproto.moderation.defs#reasonMisleading",
+        "com.atproto.moderation.defs#reasonSexual",
+        "com.atproto.moderation.defs#reasonRude",
+        "com.atproto.moderation.defs#reasonOther",
+        "com.atproto.moderation.defs#reasonAppeal",
+
+        "tools.ozone.report.defs#reasonAppeal",
+        "tools.ozone.report.defs#reasonOther",
+
+        "tools.ozone.report.defs#reasonViolenceAnimal",
+        "tools.ozone.report.defs#reasonViolenceThreats",
+        "tools.ozone.report.defs#reasonViolenceGraphicContent",
+        "tools.ozone.report.defs#reasonViolenceGlorification",
+        "tools.ozone.report.defs#reasonViolenceExtremistContent",
+        "tools.ozone.report.defs#reasonViolenceTrafficking",
+        "tools.ozone.report.defs#reasonViolenceOther",
+
+        "tools.ozone.report.defs#reasonSexualAbuseContent",
+        "tools.ozone.report.defs#reasonSexualNCII",
+        "tools.ozone.report.defs#reasonSexualDeepfake",
+        "tools.ozone.report.defs#reasonSexualAnimal",
+        "tools.ozone.report.defs#reasonSexualUnlabeled",
+        "tools.ozone.report.defs#reasonSexualOther",
+
+        "tools.ozone.report.defs#reasonChildSafetyCSAM",
+        "tools.ozone.report.defs#reasonChildSafetyGroom",
+        "tools.ozone.report.defs#reasonChildSafetyPrivacy",
+        "tools.ozone.report.defs#reasonChildSafetyHarassment",
+        "tools.ozone.report.defs#reasonChildSafetyOther",
+
+        "tools.ozone.report.defs#reasonHarassmentTroll",
+        "tools.ozone.report.defs#reasonHarassmentTargeted",
+        "tools.ozone.report.defs#reasonHarassmentHateSpeech",
+        "tools.ozone.report.defs#reasonHarassmentDoxxing",
+        "tools.ozone.report.defs#reasonHarassmentOther",
+
+        "tools.ozone.report.defs#reasonMisleadingBot",
+        "tools.ozone.report.defs#reasonMisleadingImpersonation",
+        "tools.ozone.report.defs#reasonMisleadingSpam",
+        "tools.ozone.report.defs#reasonMisleadingScam",
+        "tools.ozone.report.defs#reasonMisleadingElections",
+        "tools.ozone.report.defs#reasonMisleadingOther",
+
+        "tools.ozone.report.defs#reasonRuleSiteSecurity",
+        "tools.ozone.report.defs#reasonRuleProhibitedSales",
+        "tools.ozone.report.defs#reasonRuleBanEvasion",
+        "tools.ozone.report.defs#reasonRuleOther",
+
+        "tools.ozone.report.defs#reasonSelfHarmContent",
+        "tools.ozone.report.defs#reasonSelfHarmED",
+        "tools.ozone.report.defs#reasonSelfHarmStunts",
+        "tools.ozone.report.defs#reasonSelfHarmSubstances",
+        "tools.ozone.report.defs#reasonSelfHarmOther"
+      ]
+    },
+    "reasonSpam": {
+      "type": "token",
+      "description": "Spam: frequent unwanted promotion, replies, mentions. Prefer new lexicon definition `tools.ozone.report.defs#reasonMisleadingSpam`."
+    },
+    "reasonViolation": {
+      "type": "token",
+      "description": "Direct violation of server rules, laws, terms of service. Prefer new lexicon definition `tools.ozone.report.defs#reasonRuleOther`."
+    },
+    "reasonMisleading": {
+      "type": "token",
+      "description": "Misleading identity, affiliation, or content. Prefer new lexicon definition `tools.ozone.report.defs#reasonMisleadingOther`."
+    },
+    "reasonSexual": {
+      "type": "token",
+      "description": "Unwanted or mislabeled sexual content. Prefer new lexicon definition `tools.ozone.report.defs#reasonSexualUnlabeled`."
+    },
+    "reasonRude": {
+      "type": "token",
+      "description": "Rude, harassing, explicit, or otherwise unwelcoming behavior. Prefer new lexicon definition `tools.ozone.report.defs#reasonHarassmentOther`."
+    },
+    "reasonOther": {
+      "type": "token",
+      "description": "Reports not falling under another report category. Prefer new lexicon definition `tools.ozone.report.defs#reasonOther`."
+    },
+    "reasonAppeal": {
+      "type": "token",
+      "description": "Appeal a previously taken moderation action"
+    },
+    "subjectType": {
+      "type": "string",
+      "description": "Tag describing a type of subject that might be reported.",
+      "knownValues": ["account", "record", "chat"]
+    }
+  }
+}
diff --git a/lexicons/com/atproto/repo/applyWrites.json b/lexicons/com/atproto/repo/applyWrites.json
new file mode 100644
index 00000000..11a1f1fd
--- /dev/null
+++ b/lexicons/com/atproto/repo/applyWrites.json
@@ -0,0 +1,131 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.repo.applyWrites",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Apply a batch transaction of repository creates, updates, and deletes. Requires auth, implemented by PDS.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["repo", "writes"],
+          "properties": {
+            "repo": {
+              "type": "string",
+              "format": "at-identifier",
+              "description": "The handle or DID of the repo (aka, current account)."
+            },
+            "validate": {
+              "type": "boolean",
+              "description": "Can be set to 'false' to skip Lexicon schema validation of record data across all operations, 'true' to require it, or leave unset to validate only for known Lexicons."
+            },
+            "writes": {
+              "type": "array",
+              "items": {
+                "type": "union",
+                "refs": ["#create", "#update", "#delete"],
+                "closed": true
+              }
+            },
+            "swapCommit": {
+              "type": "string",
+              "description": "If provided, the entire operation will fail if the current repo commit CID does not match this value. Used to prevent conflicting repo mutations.",
+              "format": "cid"
+            }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": [],
+          "properties": {
+            "commit": {
+              "type": "ref",
+              "ref": "com.atproto.repo.defs#commitMeta"
+            },
+            "results": {
+              "type": "array",
+              "items": {
+                "type": "union",
+                "refs": ["#createResult", "#updateResult", "#deleteResult"],
+                "closed": true
+              }
+            }
+          }
+        }
+      },
+      "errors": [
+        {
+          "name": "InvalidSwap",
+          "description": "Indicates that the 'swapCommit' parameter did not match current commit."
+        }
+      ]
+    },
+    "create": {
+      "type": "object",
+      "description": "Operation which creates a new record.",
+      "required": ["collection", "value"],
+      "properties": {
+        "collection": { "type": "string", "format": "nsid" },
+        "rkey": {
+          "type": "string",
+          "maxLength": 512,
+          "format": "record-key",
+          "description": "NOTE: maxLength is redundant with record-key format. Keeping it temporarily to ensure backwards compatibility."
+        },
+        "value": { "type": "unknown" }
+      }
+    },
+    "update": {
+      "type": "object",
+      "description": "Operation which updates an existing record.",
+      "required": ["collection", "rkey", "value"],
+      "properties": {
+        "collection": { "type": "string", "format": "nsid" },
+        "rkey": { "type": "string", "format": "record-key" },
+        "value": { "type": "unknown" }
+      }
+    },
+    "delete": {
+      "type": "object",
+      "description": "Operation which deletes an existing record.",
+      "required": ["collection", "rkey"],
+      "properties": {
+        "collection": { "type": "string", "format": "nsid" },
+        "rkey": { "type": "string", "format": "record-key" }
+      }
+    },
+    "createResult": {
+      "type": "object",
+      "required": ["uri", "cid"],
+      "properties": {
+        "uri": { "type": "string", "format": "at-uri" },
+        "cid": { "type": "string", "format": "cid" },
+        "validationStatus": {
+          "type": "string",
+          "knownValues": ["valid", "unknown"]
+        }
+      }
+    },
+    "updateResult": {
+      "type": "object",
+      "required": ["uri", "cid"],
+      "properties": {
+        "uri": { "type": "string", "format": "at-uri" },
+        "cid": { "type": "string", "format": "cid" },
+        "validationStatus": {
+          "type": "string",
+          "knownValues": ["valid", "unknown"]
+        }
+      }
+    },
+    "deleteResult": {
+      "type": "object",
+      "required": [],
+      "properties": {}
+    }
+  }
+}
diff --git a/lexicons/com/atproto/repo/createRecord.json b/lexicons/com/atproto/repo/createRecord.json
new file mode 100644
index 00000000..de5f4081
--- /dev/null
+++ b/lexicons/com/atproto/repo/createRecord.json
@@ -0,0 +1,73 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.repo.createRecord",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Create a single new repository record. Requires auth, implemented by PDS.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["repo", "collection", "record"],
+          "properties": {
+            "repo": {
+              "type": "string",
+              "format": "at-identifier",
+              "description": "The handle or DID of the repo (aka, current account)."
+            },
+            "collection": {
+              "type": "string",
+              "format": "nsid",
+              "description": "The NSID of the record collection."
+            },
+            "rkey": {
+              "type": "string",
+              "format": "record-key",
+              "description": "The Record Key.",
+              "maxLength": 512
+            },
+            "validate": {
+              "type": "boolean",
+              "description": "Can be set to 'false' to skip Lexicon schema validation of record data, 'true' to require it, or leave unset to validate only for known Lexicons."
+            },
+            "record": {
+              "type": "unknown",
+              "description": "The record itself. Must contain a $type field."
+            },
+            "swapCommit": {
+              "type": "string",
+              "format": "cid",
+              "description": "Compare and swap with the previous commit by CID."
+            }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["uri", "cid"],
+          "properties": {
+            "uri": { "type": "string", "format": "at-uri" },
+            "cid": { "type": "string", "format": "cid" },
+            "commit": {
+              "type": "ref",
+              "ref": "com.atproto.repo.defs#commitMeta"
+            },
+            "validationStatus": {
+              "type": "string",
+              "knownValues": ["valid", "unknown"]
+            }
+          }
+        }
+      },
+      "errors": [
+        {
+          "name": "InvalidSwap",
+          "description": "Indicates that 'swapCommit' didn't match current repo commit."
+        }
+      ]
+    }
+  }
+}
diff --git a/lexicons/com/atproto/repo/defs.json b/lexicons/com/atproto/repo/defs.json
new file mode 100644
index 00000000..8752599f
--- /dev/null
+++ b/lexicons/com/atproto/repo/defs.json
@@ -0,0 +1,14 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.repo.defs",
+  "defs": {
+    "commitMeta": {
+      "type": "object",
+      "required": ["cid", "rev"],
+      "properties": {
+        "cid": { "type": "string", "format": "cid" },
+        "rev": { "type": "string", "format": "tid" }
+      }
+    }
+  }
+}
diff --git a/lexicons/com/atproto/repo/deleteRecord.json b/lexicons/com/atproto/repo/deleteRecord.json
new file mode 100644
index 00000000..9831da17
--- /dev/null
+++ b/lexicons/com/atproto/repo/deleteRecord.json
@@ -0,0 +1,57 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.repo.deleteRecord",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Delete a repository record, or ensure it doesn't exist. Requires auth, implemented by PDS.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["repo", "collection", "rkey"],
+          "properties": {
+            "repo": {
+              "type": "string",
+              "format": "at-identifier",
+              "description": "The handle or DID of the repo (aka, current account)."
+            },
+            "collection": {
+              "type": "string",
+              "format": "nsid",
+              "description": "The NSID of the record collection."
+            },
+            "rkey": {
+              "type": "string",
+              "format": "record-key",
+              "description": "The Record Key."
+            },
+            "swapRecord": {
+              "type": "string",
+              "format": "cid",
+              "description": "Compare and swap with the previous record by CID."
+            },
+            "swapCommit": {
+              "type": "string",
+              "format": "cid",
+              "description": "Compare and swap with the previous commit by CID."
+            }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "properties": {
+            "commit": {
+              "type": "ref",
+              "ref": "com.atproto.repo.defs#commitMeta"
+            }
+          }
+        }
+      },
+      "errors": [{ "name": "InvalidSwap" }]
+    }
+  }
+}
diff --git a/lexicons/com/atproto/repo/describeRepo.json b/lexicons/com/atproto/repo/describeRepo.json
new file mode 100644
index 00000000..b1ce2b6c
--- /dev/null
+++ b/lexicons/com/atproto/repo/describeRepo.json
@@ -0,0 +1,51 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.repo.describeRepo",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get information about an account and repository, including the list of collections. Does not require auth.",
+      "parameters": {
+        "type": "params",
+        "required": ["repo"],
+        "properties": {
+          "repo": {
+            "type": "string",
+            "format": "at-identifier",
+            "description": "The handle or DID of the repo."
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": [
+            "handle",
+            "did",
+            "didDoc",
+            "collections",
+            "handleIsCorrect"
+          ],
+          "properties": {
+            "handle": { "type": "string", "format": "handle" },
+            "did": { "type": "string", "format": "did" },
+            "didDoc": {
+              "type": "unknown",
+              "description": "The complete DID document for this account."
+            },
+            "collections": {
+              "type": "array",
+              "description": "List of all the collections (NSIDs) for which this repo contains at least one record.",
+              "items": { "type": "string", "format": "nsid" }
+            },
+            "handleIsCorrect": {
+              "type": "boolean",
+              "description": "Indicates if handle is currently valid (resolves bi-directionally)"
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/com/atproto/repo/getRecord.json b/lexicons/com/atproto/repo/getRecord.json
new file mode 100644
index 00000000..388b9c5a
--- /dev/null
+++ b/lexicons/com/atproto/repo/getRecord.json
@@ -0,0 +1,49 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.repo.getRecord",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get a single record from a repository. Does not require auth.",
+      "parameters": {
+        "type": "params",
+        "required": ["repo", "collection", "rkey"],
+        "properties": {
+          "repo": {
+            "type": "string",
+            "format": "at-identifier",
+            "description": "The handle or DID of the repo."
+          },
+          "collection": {
+            "type": "string",
+            "format": "nsid",
+            "description": "The NSID of the record collection."
+          },
+          "rkey": {
+            "type": "string",
+            "description": "The Record Key.",
+            "format": "record-key"
+          },
+          "cid": {
+            "type": "string",
+            "format": "cid",
+            "description": "The CID of the version of the record. If not specified, then return the most recent version."
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["uri", "value"],
+          "properties": {
+            "uri": { "type": "string", "format": "at-uri" },
+            "cid": { "type": "string", "format": "cid" },
+            "value": { "type": "unknown" }
+          }
+        }
+      },
+      "errors": [{ "name": "RecordNotFound" }]
+    }
+  }
+}
diff --git a/lexicons/com/atproto/repo/importRepo.json b/lexicons/com/atproto/repo/importRepo.json
new file mode 100644
index 00000000..fc850b1a
--- /dev/null
+++ b/lexicons/com/atproto/repo/importRepo.json
@@ -0,0 +1,13 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.repo.importRepo",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Import a repo in the form of a CAR file. Requires Content-Length HTTP header to be set.",
+      "input": {
+        "encoding": "application/vnd.ipld.car"
+      }
+    }
+  }
+}
diff --git a/lexicons/com/atproto/repo/listMissingBlobs.json b/lexicons/com/atproto/repo/listMissingBlobs.json
new file mode 100644
index 00000000..c39913d5
--- /dev/null
+++ b/lexicons/com/atproto/repo/listMissingBlobs.json
@@ -0,0 +1,44 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.repo.listMissingBlobs",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Returns a list of missing blobs for the requesting account. Intended to be used in the account migration flow.",
+      "parameters": {
+        "type": "params",
+        "properties": {
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 1000,
+            "default": 500
+          },
+          "cursor": { "type": "string" }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["blobs"],
+          "properties": {
+            "cursor": { "type": "string" },
+            "blobs": {
+              "type": "array",
+              "items": { "type": "ref", "ref": "#recordBlob" }
+            }
+          }
+        }
+      }
+    },
+    "recordBlob": {
+      "type": "object",
+      "required": ["cid", "recordUri"],
+      "properties": {
+        "cid": { "type": "string", "format": "cid" },
+        "recordUri": { "type": "string", "format": "at-uri" }
+      }
+    }
+  }
+}
diff --git a/lexicons/com/atproto/repo/listRecords.json b/lexicons/com/atproto/repo/listRecords.json
new file mode 100644
index 00000000..3d43ff40
--- /dev/null
+++ b/lexicons/com/atproto/repo/listRecords.json
@@ -0,0 +1,61 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.repo.listRecords",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "List a range of records in a repository, matching a specific collection. Does not require auth.",
+      "parameters": {
+        "type": "params",
+        "required": ["repo", "collection"],
+        "properties": {
+          "repo": {
+            "type": "string",
+            "format": "at-identifier",
+            "description": "The handle or DID of the repo."
+          },
+          "collection": {
+            "type": "string",
+            "format": "nsid",
+            "description": "The NSID of the record type."
+          },
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 100,
+            "default": 50,
+            "description": "The number of records to return."
+          },
+          "cursor": { "type": "string" },
+          "reverse": {
+            "type": "boolean",
+            "description": "Flag to reverse the order of the returned records."
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["records"],
+          "properties": {
+            "cursor": { "type": "string" },
+            "records": {
+              "type": "array",
+              "items": { "type": "ref", "ref": "#record" }
+            }
+          }
+        }
+      }
+    },
+    "record": {
+      "type": "object",
+      "required": ["uri", "cid", "value"],
+      "properties": {
+        "uri": { "type": "string", "format": "at-uri" },
+        "cid": { "type": "string", "format": "cid" },
+        "value": { "type": "unknown" }
+      }
+    }
+  }
+}
diff --git a/lexicons/com/atproto/repo/putRecord.json b/lexicons/com/atproto/repo/putRecord.json
new file mode 100644
index 00000000..830fade5
--- /dev/null
+++ b/lexicons/com/atproto/repo/putRecord.json
@@ -0,0 +1,74 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.repo.putRecord",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Write a repository record, creating or updating it as needed. Requires auth, implemented by PDS.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["repo", "collection", "rkey", "record"],
+          "nullable": ["swapRecord"],
+          "properties": {
+            "repo": {
+              "type": "string",
+              "format": "at-identifier",
+              "description": "The handle or DID of the repo (aka, current account)."
+            },
+            "collection": {
+              "type": "string",
+              "format": "nsid",
+              "description": "The NSID of the record collection."
+            },
+            "rkey": {
+              "type": "string",
+              "format": "record-key",
+              "description": "The Record Key.",
+              "maxLength": 512
+            },
+            "validate": {
+              "type": "boolean",
+              "description": "Can be set to 'false' to skip Lexicon schema validation of record data, 'true' to require it, or leave unset to validate only for known Lexicons."
+            },
+            "record": {
+              "type": "unknown",
+              "description": "The record to write."
+            },
+            "swapRecord": {
+              "type": "string",
+              "format": "cid",
+              "description": "Compare and swap with the previous record by CID. WARNING: nullable and optional field; may cause problems with golang implementation"
+            },
+            "swapCommit": {
+              "type": "string",
+              "format": "cid",
+              "description": "Compare and swap with the previous commit by CID."
+            }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["uri", "cid"],
+          "properties": {
+            "uri": { "type": "string", "format": "at-uri" },
+            "cid": { "type": "string", "format": "cid" },
+            "commit": {
+              "type": "ref",
+              "ref": "com.atproto.repo.defs#commitMeta"
+            },
+            "validationStatus": {
+              "type": "string",
+              "knownValues": ["valid", "unknown"]
+            }
+          }
+        }
+      },
+      "errors": [{ "name": "InvalidSwap" }]
+    }
+  }
+}
diff --git a/lexicons/com/atproto/repo/strongRef.json b/lexicons/com/atproto/repo/strongRef.json
new file mode 100644
index 00000000..cb796251
--- /dev/null
+++ b/lexicons/com/atproto/repo/strongRef.json
@@ -0,0 +1,15 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.repo.strongRef",
+  "description": "A URI with a content-hash fingerprint.",
+  "defs": {
+    "main": {
+      "type": "object",
+      "required": ["uri", "cid"],
+      "properties": {
+        "uri": { "type": "string", "format": "at-uri" },
+        "cid": { "type": "string", "format": "cid" }
+      }
+    }
+  }
+}
diff --git a/lexicons/com/atproto/repo/uploadBlob.json b/lexicons/com/atproto/repo/uploadBlob.json
new file mode 100644
index 00000000..547a995a
--- /dev/null
+++ b/lexicons/com/atproto/repo/uploadBlob.json
@@ -0,0 +1,23 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.repo.uploadBlob",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Upload a new blob, to be referenced from a repository record. The blob will be deleted if it is not referenced within a time window (eg, minutes). Blob restrictions (mimetype, size, etc) are enforced when the reference is created. Requires auth, implemented by PDS.",
+      "input": {
+        "encoding": "*/*"
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["blob"],
+          "properties": {
+            "blob": { "type": "blob" }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/com/atproto/server/activateAccount.json b/lexicons/com/atproto/server/activateAccount.json
new file mode 100644
index 00000000..e06935fc
--- /dev/null
+++ b/lexicons/com/atproto/server/activateAccount.json
@@ -0,0 +1,10 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.server.activateAccount",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Activates a currently deactivated account. Used to finalize account migration after the account's repo is imported and identity is setup."
+    }
+  }
+}
diff --git a/lexicons/com/atproto/server/checkAccountStatus.json b/lexicons/com/atproto/server/checkAccountStatus.json
new file mode 100644
index 00000000..d34596e6
--- /dev/null
+++ b/lexicons/com/atproto/server/checkAccountStatus.json
@@ -0,0 +1,38 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.server.checkAccountStatus",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Returns the status of an account, especially as pertaining to import or recovery. Can be called many times over the course of an account migration. Requires auth and can only be called pertaining to oneself.",
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": [
+            "activated",
+            "validDid",
+            "repoCommit",
+            "repoRev",
+            "repoBlocks",
+            "indexedRecords",
+            "privateStateValues",
+            "expectedBlobs",
+            "importedBlobs"
+          ],
+          "properties": {
+            "activated": { "type": "boolean" },
+            "validDid": { "type": "boolean" },
+            "repoCommit": { "type": "string", "format": "cid" },
+            "repoRev": { "type": "string" },
+            "repoBlocks": { "type": "integer" },
+            "indexedRecords": { "type": "integer" },
+            "privateStateValues": { "type": "integer" },
+            "expectedBlobs": { "type": "integer" },
+            "importedBlobs": { "type": "integer" }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/com/atproto/server/confirmEmail.json b/lexicons/com/atproto/server/confirmEmail.json
new file mode 100644
index 00000000..6c2e4291
--- /dev/null
+++ b/lexicons/com/atproto/server/confirmEmail.json
@@ -0,0 +1,27 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.server.confirmEmail",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Confirm an email using a token from com.atproto.server.requestEmailConfirmation.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["email", "token"],
+          "properties": {
+            "email": { "type": "string" },
+            "token": { "type": "string" }
+          }
+        }
+      },
+      "errors": [
+        { "name": "AccountNotFound" },
+        { "name": "ExpiredToken" },
+        { "name": "InvalidToken" },
+        { "name": "InvalidEmail" }
+      ]
+    }
+  }
+}
diff --git a/lexicons/com/atproto/server/createAccount.json b/lexicons/com/atproto/server/createAccount.json
new file mode 100644
index 00000000..b32bbe15
--- /dev/null
+++ b/lexicons/com/atproto/server/createAccount.json
@@ -0,0 +1,76 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.server.createAccount",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Create an account. Implemented by PDS.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["handle"],
+          "properties": {
+            "email": { "type": "string" },
+            "handle": {
+              "type": "string",
+              "format": "handle",
+              "description": "Requested handle for the account."
+            },
+            "did": {
+              "type": "string",
+              "format": "did",
+              "description": "Pre-existing atproto DID, being imported to a new account."
+            },
+            "inviteCode": { "type": "string" },
+            "verificationCode": { "type": "string" },
+            "verificationPhone": { "type": "string" },
+            "password": {
+              "type": "string",
+              "description": "Initial account password. May need to meet instance-specific password strength requirements."
+            },
+            "recoveryKey": {
+              "type": "string",
+              "description": "DID PLC rotation key (aka, recovery key) to be included in PLC creation operation."
+            },
+            "plcOp": {
+              "type": "unknown",
+              "description": "A signed DID PLC operation to be submitted as part of importing an existing account to this instance. NOTE: this optional field may be updated when full account migration is implemented."
+            }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "description": "Account login session returned on successful account creation.",
+          "required": ["accessJwt", "refreshJwt", "handle", "did"],
+          "properties": {
+            "accessJwt": { "type": "string" },
+            "refreshJwt": { "type": "string" },
+            "handle": { "type": "string", "format": "handle" },
+            "did": {
+              "type": "string",
+              "format": "did",
+              "description": "The DID of the new account."
+            },
+            "didDoc": {
+              "type": "unknown",
+              "description": "Complete DID document."
+            }
+          }
+        }
+      },
+      "errors": [
+        { "name": "InvalidHandle" },
+        { "name": "InvalidPassword" },
+        { "name": "InvalidInviteCode" },
+        { "name": "HandleNotAvailable" },
+        { "name": "UnsupportedDomain" },
+        { "name": "UnresolvableDid" },
+        { "name": "IncompatibleDidDoc" }
+      ]
+    }
+  }
+}
diff --git a/lexicons/com/atproto/server/createAppPassword.json b/lexicons/com/atproto/server/createAppPassword.json
new file mode 100644
index 00000000..f40cf59a
--- /dev/null
+++ b/lexicons/com/atproto/server/createAppPassword.json
@@ -0,0 +1,45 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.server.createAppPassword",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Create an App Password.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["name"],
+          "properties": {
+            "name": {
+              "type": "string",
+              "description": "A short name for the App Password, to help distinguish them."
+            },
+            "privileged": {
+              "type": "boolean",
+              "description": "If an app password has 'privileged' access to possibly sensitive account state. Meant for use with trusted clients."
+            }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "ref",
+          "ref": "#appPassword"
+        }
+      },
+      "errors": [{ "name": "AccountTakedown" }]
+    },
+    "appPassword": {
+      "type": "object",
+      "required": ["name", "password", "createdAt"],
+      "properties": {
+        "name": { "type": "string" },
+        "password": { "type": "string" },
+        "createdAt": { "type": "string", "format": "datetime" },
+        "privileged": { "type": "boolean" }
+      }
+    }
+  }
+}
diff --git a/lexicons/com/atproto/server/createInviteCode.json b/lexicons/com/atproto/server/createInviteCode.json
new file mode 100644
index 00000000..5ce5dbe3
--- /dev/null
+++ b/lexicons/com/atproto/server/createInviteCode.json
@@ -0,0 +1,31 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.server.createInviteCode",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Create an invite code.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["useCount"],
+          "properties": {
+            "useCount": { "type": "integer" },
+            "forAccount": { "type": "string", "format": "did" }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["code"],
+          "properties": {
+            "code": { "type": "string" }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/com/atproto/server/createInviteCodes.json b/lexicons/com/atproto/server/createInviteCodes.json
new file mode 100644
index 00000000..1965fe99
--- /dev/null
+++ b/lexicons/com/atproto/server/createInviteCodes.json
@@ -0,0 +1,49 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.server.createInviteCodes",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Create invite codes.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["codeCount", "useCount"],
+          "properties": {
+            "codeCount": { "type": "integer", "default": 1 },
+            "useCount": { "type": "integer" },
+            "forAccounts": {
+              "type": "array",
+              "items": { "type": "string", "format": "did" }
+            }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["codes"],
+          "properties": {
+            "codes": {
+              "type": "array",
+              "items": { "type": "ref", "ref": "#accountCodes" }
+            }
+          }
+        }
+      }
+    },
+    "accountCodes": {
+      "type": "object",
+      "required": ["account", "codes"],
+      "properties": {
+        "account": { "type": "string" },
+        "codes": {
+          "type": "array",
+          "items": { "type": "string" }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/com/atproto/server/createSession.json b/lexicons/com/atproto/server/createSession.json
new file mode 100644
index 00000000..8cde0863
--- /dev/null
+++ b/lexicons/com/atproto/server/createSession.json
@@ -0,0 +1,56 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.server.createSession",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Create an authentication session.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["identifier", "password"],
+          "properties": {
+            "identifier": {
+              "type": "string",
+              "description": "Handle or other identifier supported by the server for the authenticating user."
+            },
+            "password": { "type": "string" },
+            "authFactorToken": { "type": "string" },
+            "allowTakendown": {
+              "type": "boolean",
+              "description": "When true, instead of throwing error for takendown accounts, a valid response with a narrow scoped token will be returned"
+            }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["accessJwt", "refreshJwt", "handle", "did"],
+          "properties": {
+            "accessJwt": { "type": "string" },
+            "refreshJwt": { "type": "string" },
+            "handle": { "type": "string", "format": "handle" },
+            "did": { "type": "string", "format": "did" },
+            "didDoc": { "type": "unknown" },
+            "email": { "type": "string" },
+            "emailConfirmed": { "type": "boolean" },
+            "emailAuthFactor": { "type": "boolean" },
+            "active": { "type": "boolean" },
+            "status": {
+              "type": "string",
+              "description": "If active=false, this optional field indicates a possible reason for why the account is not active. If active=false and no status is supplied, then the host makes no claim for why the repository is no longer being hosted.",
+              "knownValues": ["takendown", "suspended", "deactivated"]
+            }
+          }
+        }
+      },
+      "errors": [
+        { "name": "AccountTakedown" },
+        { "name": "AuthFactorTokenRequired" }
+      ]
+    }
+  }
+}
diff --git a/lexicons/com/atproto/server/deactivateAccount.json b/lexicons/com/atproto/server/deactivateAccount.json
new file mode 100644
index 00000000..698fccf2
--- /dev/null
+++ b/lexicons/com/atproto/server/deactivateAccount.json
@@ -0,0 +1,23 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.server.deactivateAccount",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Deactivates a currently active account. Stops serving of repo, and future writes to repo until reactivated. Used to finalize account migration with the old host after the account has been activated on the new host.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "properties": {
+            "deleteAfter": {
+              "type": "string",
+              "format": "datetime",
+              "description": "A recommendation to server as to how long they should hold onto the deactivated account before deleting."
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/com/atproto/server/defs.json b/lexicons/com/atproto/server/defs.json
new file mode 100644
index 00000000..8686d4d4
--- /dev/null
+++ b/lexicons/com/atproto/server/defs.json
@@ -0,0 +1,38 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.server.defs",
+  "defs": {
+    "inviteCode": {
+      "type": "object",
+      "required": [
+        "code",
+        "available",
+        "disabled",
+        "forAccount",
+        "createdBy",
+        "createdAt",
+        "uses"
+      ],
+      "properties": {
+        "code": { "type": "string" },
+        "available": { "type": "integer" },
+        "disabled": { "type": "boolean" },
+        "forAccount": { "type": "string" },
+        "createdBy": { "type": "string" },
+        "createdAt": { "type": "string", "format": "datetime" },
+        "uses": {
+          "type": "array",
+          "items": { "type": "ref", "ref": "#inviteCodeUse" }
+        }
+      }
+    },
+    "inviteCodeUse": {
+      "type": "object",
+      "required": ["usedBy", "usedAt"],
+      "properties": {
+        "usedBy": { "type": "string", "format": "did" },
+        "usedAt": { "type": "string", "format": "datetime" }
+      }
+    }
+  }
+}
diff --git a/lexicons/com/atproto/server/deleteAccount.json b/lexicons/com/atproto/server/deleteAccount.json
new file mode 100644
index 00000000..cf4babfe
--- /dev/null
+++ b/lexicons/com/atproto/server/deleteAccount.json
@@ -0,0 +1,23 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.server.deleteAccount",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Delete an actor's account with a token and password. Can only be called after requesting a deletion token. Requires auth.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["did", "password", "token"],
+          "properties": {
+            "did": { "type": "string", "format": "did" },
+            "password": { "type": "string" },
+            "token": { "type": "string" }
+          }
+        }
+      },
+      "errors": [{ "name": "ExpiredToken" }, { "name": "InvalidToken" }]
+    }
+  }
+}
diff --git a/lexicons/com/atproto/server/deleteSession.json b/lexicons/com/atproto/server/deleteSession.json
new file mode 100644
index 00000000..807a89dc
--- /dev/null
+++ b/lexicons/com/atproto/server/deleteSession.json
@@ -0,0 +1,10 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.server.deleteSession",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Delete the current session. Requires auth."
+    }
+  }
+}
diff --git a/lexicons/com/atproto/server/describeServer.json b/lexicons/com/atproto/server/describeServer.json
new file mode 100644
index 00000000..bbecf651
--- /dev/null
+++ b/lexicons/com/atproto/server/describeServer.json
@@ -0,0 +1,59 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.server.describeServer",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Describes the server's account creation requirements and capabilities. Implemented by PDS.",
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["did", "availableUserDomains"],
+          "properties": {
+            "inviteCodeRequired": {
+              "type": "boolean",
+              "description": "If true, an invite code must be supplied to create an account on this instance."
+            },
+            "phoneVerificationRequired": {
+              "type": "boolean",
+              "description": "If true, a phone verification token must be supplied to create an account on this instance."
+            },
+            "availableUserDomains": {
+              "type": "array",
+              "description": "List of domain suffixes that can be used in account handles.",
+              "items": { "type": "string" }
+            },
+            "links": {
+              "type": "ref",
+              "description": "URLs of service policy documents.",
+              "ref": "#links"
+            },
+            "contact": {
+              "type": "ref",
+              "description": "Contact information",
+              "ref": "#contact"
+            },
+            "did": {
+              "type": "string",
+              "format": "did"
+            }
+          }
+        }
+      }
+    },
+    "links": {
+      "type": "object",
+      "properties": {
+        "privacyPolicy": { "type": "string", "format": "uri" },
+        "termsOfService": { "type": "string", "format": "uri" }
+      }
+    },
+    "contact": {
+      "type": "object",
+      "properties": {
+        "email": { "type": "string" }
+      }
+    }
+  }
+}
diff --git a/lexicons/com/atproto/server/getAccountInviteCodes.json b/lexicons/com/atproto/server/getAccountInviteCodes.json
new file mode 100644
index 00000000..72f08227
--- /dev/null
+++ b/lexicons/com/atproto/server/getAccountInviteCodes.json
@@ -0,0 +1,38 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.server.getAccountInviteCodes",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get all invite codes for the current account. Requires auth.",
+      "parameters": {
+        "type": "params",
+        "properties": {
+          "includeUsed": { "type": "boolean", "default": true },
+          "createAvailable": {
+            "type": "boolean",
+            "default": true,
+            "description": "Controls whether any new 'earned' but not 'created' invites should be created."
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["codes"],
+          "properties": {
+            "codes": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "com.atproto.server.defs#inviteCode"
+              }
+            }
+          }
+        }
+      },
+      "errors": [{ "name": "DuplicateCreate" }]
+    }
+  }
+}
diff --git a/lexicons/com/atproto/server/getServiceAuth.json b/lexicons/com/atproto/server/getServiceAuth.json
new file mode 100644
index 00000000..37c150d7
--- /dev/null
+++ b/lexicons/com/atproto/server/getServiceAuth.json
@@ -0,0 +1,48 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.server.getServiceAuth",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get a signed token on behalf of the requesting DID for the requested service.",
+      "parameters": {
+        "type": "params",
+        "required": ["aud"],
+        "properties": {
+          "aud": {
+            "type": "string",
+            "format": "did",
+            "description": "The DID of the service that the token will be used to authenticate with"
+          },
+          "exp": {
+            "type": "integer",
+            "description": "The time in Unix Epoch seconds that the JWT expires. Defaults to 60 seconds in the future. The service may enforce certain time bounds on tokens depending on the requested scope."
+          },
+          "lxm": {
+            "type": "string",
+            "format": "nsid",
+            "description": "Lexicon (XRPC) method to bind the requested token to"
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["token"],
+          "properties": {
+            "token": {
+              "type": "string"
+            }
+          }
+        }
+      },
+      "errors": [
+        {
+          "name": "BadExpiration",
+          "description": "Indicates that the requested expiration date is not a valid. May be in the past or may be reliant on the requested scopes."
+        }
+      ]
+    }
+  }
+}
diff --git a/lexicons/com/atproto/server/getSession.json b/lexicons/com/atproto/server/getSession.json
new file mode 100644
index 00000000..86efa746
--- /dev/null
+++ b/lexicons/com/atproto/server/getSession.json
@@ -0,0 +1,31 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.server.getSession",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get information about the current auth session. Requires auth.",
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["handle", "did"],
+          "properties": {
+            "handle": { "type": "string", "format": "handle" },
+            "did": { "type": "string", "format": "did" },
+            "email": { "type": "string" },
+            "emailConfirmed": { "type": "boolean" },
+            "emailAuthFactor": { "type": "boolean" },
+            "didDoc": { "type": "unknown" },
+            "active": { "type": "boolean" },
+            "status": {
+              "type": "string",
+              "description": "If active=false, this optional field indicates a possible reason for why the account is not active. If active=false and no status is supplied, then the host makes no claim for why the repository is no longer being hosted.",
+              "knownValues": ["takendown", "suspended", "deactivated"]
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/com/atproto/server/listAppPasswords.json b/lexicons/com/atproto/server/listAppPasswords.json
new file mode 100644
index 00000000..1c2ae629
--- /dev/null
+++ b/lexicons/com/atproto/server/listAppPasswords.json
@@ -0,0 +1,33 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.server.listAppPasswords",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "List all App Passwords.",
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["passwords"],
+          "properties": {
+            "passwords": {
+              "type": "array",
+              "items": { "type": "ref", "ref": "#appPassword" }
+            }
+          }
+        }
+      },
+      "errors": [{ "name": "AccountTakedown" }]
+    },
+    "appPassword": {
+      "type": "object",
+      "required": ["name", "createdAt"],
+      "properties": {
+        "name": { "type": "string" },
+        "createdAt": { "type": "string", "format": "datetime" },
+        "privileged": { "type": "boolean" }
+      }
+    }
+  }
+}
diff --git a/lexicons/com/atproto/server/refreshSession.json b/lexicons/com/atproto/server/refreshSession.json
new file mode 100644
index 00000000..71123a4f
--- /dev/null
+++ b/lexicons/com/atproto/server/refreshSession.json
@@ -0,0 +1,31 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.server.refreshSession",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Refresh an authentication session. Requires auth using the 'refreshJwt' (not the 'accessJwt').",
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["accessJwt", "refreshJwt", "handle", "did"],
+          "properties": {
+            "accessJwt": { "type": "string" },
+            "refreshJwt": { "type": "string" },
+            "handle": { "type": "string", "format": "handle" },
+            "did": { "type": "string", "format": "did" },
+            "didDoc": { "type": "unknown" },
+            "active": { "type": "boolean" },
+            "status": {
+              "type": "string",
+              "description": "Hosting status of the account. If not specified, then assume 'active'.",
+              "knownValues": ["takendown", "suspended", "deactivated"]
+            }
+          }
+        }
+      },
+      "errors": [{ "name": "AccountTakedown" }]
+    }
+  }
+}
diff --git a/lexicons/com/atproto/server/requestAccountDelete.json b/lexicons/com/atproto/server/requestAccountDelete.json
new file mode 100644
index 00000000..aaac0b70
--- /dev/null
+++ b/lexicons/com/atproto/server/requestAccountDelete.json
@@ -0,0 +1,10 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.server.requestAccountDelete",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Initiate a user account deletion via email."
+    }
+  }
+}
diff --git a/lexicons/com/atproto/server/requestEmailConfirmation.json b/lexicons/com/atproto/server/requestEmailConfirmation.json
new file mode 100644
index 00000000..0020ed31
--- /dev/null
+++ b/lexicons/com/atproto/server/requestEmailConfirmation.json
@@ -0,0 +1,10 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.server.requestEmailConfirmation",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Request an email with a code to confirm ownership of email."
+    }
+  }
+}
diff --git a/lexicons/com/atproto/server/requestEmailUpdate.json b/lexicons/com/atproto/server/requestEmailUpdate.json
new file mode 100644
index 00000000..4cc1a86f
--- /dev/null
+++ b/lexicons/com/atproto/server/requestEmailUpdate.json
@@ -0,0 +1,20 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.server.requestEmailUpdate",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Request a token in order to update email.",
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["tokenRequired"],
+          "properties": {
+            "tokenRequired": { "type": "boolean" }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/com/atproto/server/requestPasswordReset.json b/lexicons/com/atproto/server/requestPasswordReset.json
new file mode 100644
index 00000000..41f717e9
--- /dev/null
+++ b/lexicons/com/atproto/server/requestPasswordReset.json
@@ -0,0 +1,20 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.server.requestPasswordReset",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Initiate a user account password reset via email.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["email"],
+          "properties": {
+            "email": { "type": "string" }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/com/atproto/server/reserveSigningKey.json b/lexicons/com/atproto/server/reserveSigningKey.json
new file mode 100644
index 00000000..a33e1ede
--- /dev/null
+++ b/lexicons/com/atproto/server/reserveSigningKey.json
@@ -0,0 +1,36 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.server.reserveSigningKey",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Reserve a repo signing key, for use with account creation. Necessary so that a DID PLC update operation can be constructed during an account migraiton. Public and does not require auth; implemented by PDS. NOTE: this endpoint may change when full account migration is implemented.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "properties": {
+            "did": {
+              "type": "string",
+              "format": "did",
+              "description": "The DID to reserve a key for."
+            }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["signingKey"],
+          "properties": {
+            "signingKey": {
+              "type": "string",
+              "description": "The public key for the reserved signing key, in did:key serialization."
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/com/atproto/server/resetPassword.json b/lexicons/com/atproto/server/resetPassword.json
new file mode 100644
index 00000000..cdf7fe3e
--- /dev/null
+++ b/lexicons/com/atproto/server/resetPassword.json
@@ -0,0 +1,22 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.server.resetPassword",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Reset a user account password using a token.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["token", "password"],
+          "properties": {
+            "token": { "type": "string" },
+            "password": { "type": "string" }
+          }
+        }
+      },
+      "errors": [{ "name": "ExpiredToken" }, { "name": "InvalidToken" }]
+    }
+  }
+}
diff --git a/lexicons/com/atproto/server/revokeAppPassword.json b/lexicons/com/atproto/server/revokeAppPassword.json
new file mode 100644
index 00000000..bcfd3d86
--- /dev/null
+++ b/lexicons/com/atproto/server/revokeAppPassword.json
@@ -0,0 +1,20 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.server.revokeAppPassword",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Revoke an App Password by name.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["name"],
+          "properties": {
+            "name": { "type": "string" }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/com/atproto/server/updateEmail.json b/lexicons/com/atproto/server/updateEmail.json
new file mode 100644
index 00000000..7d90247a
--- /dev/null
+++ b/lexicons/com/atproto/server/updateEmail.json
@@ -0,0 +1,30 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.server.updateEmail",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Update an account's email.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["email"],
+          "properties": {
+            "email": { "type": "string" },
+            "emailAuthFactor": { "type": "boolean" },
+            "token": {
+              "type": "string",
+              "description": "Requires a token from com.atproto.sever.requestEmailUpdate if the account's email has been confirmed."
+            }
+          }
+        }
+      },
+      "errors": [
+        { "name": "ExpiredToken" },
+        { "name": "InvalidToken" },
+        { "name": "TokenRequired" }
+      ]
+    }
+  }
+}
diff --git a/lexicons/com/atproto/sync/defs.json b/lexicons/com/atproto/sync/defs.json
new file mode 100644
index 00000000..44261ac5
--- /dev/null
+++ b/lexicons/com/atproto/sync/defs.json
@@ -0,0 +1,10 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.sync.defs",
+  "defs": {
+    "hostStatus": {
+      "type": "string",
+      "knownValues": ["active", "idle", "offline", "throttled", "banned"]
+    }
+  }
+}
diff --git a/lexicons/com/atproto/sync/getBlob.json b/lexicons/com/atproto/sync/getBlob.json
new file mode 100644
index 00000000..501cba1e
--- /dev/null
+++ b/lexicons/com/atproto/sync/getBlob.json
@@ -0,0 +1,36 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.sync.getBlob",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get a blob associated with a given account. Returns the full blob as originally uploaded. Does not require auth; implemented by PDS.",
+      "parameters": {
+        "type": "params",
+        "required": ["did", "cid"],
+        "properties": {
+          "did": {
+            "type": "string",
+            "format": "did",
+            "description": "The DID of the account."
+          },
+          "cid": {
+            "type": "string",
+            "format": "cid",
+            "description": "The CID of the blob to fetch"
+          }
+        }
+      },
+      "output": {
+        "encoding": "*/*"
+      },
+      "errors": [
+        { "name": "BlobNotFound" },
+        { "name": "RepoNotFound" },
+        { "name": "RepoTakendown" },
+        { "name": "RepoSuspended" },
+        { "name": "RepoDeactivated" }
+      ]
+    }
+  }
+}
diff --git a/lexicons/com/atproto/sync/getBlocks.json b/lexicons/com/atproto/sync/getBlocks.json
new file mode 100644
index 00000000..aa507d74
--- /dev/null
+++ b/lexicons/com/atproto/sync/getBlocks.json
@@ -0,0 +1,35 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.sync.getBlocks",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get data blocks from a given repo, by CID. For example, intermediate MST nodes, or records. Does not require auth; implemented by PDS.",
+      "parameters": {
+        "type": "params",
+        "required": ["did", "cids"],
+        "properties": {
+          "did": {
+            "type": "string",
+            "format": "did",
+            "description": "The DID of the repo."
+          },
+          "cids": {
+            "type": "array",
+            "items": { "type": "string", "format": "cid" }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/vnd.ipld.car"
+      },
+      "errors": [
+        { "name": "BlockNotFound" },
+        { "name": "RepoNotFound" },
+        { "name": "RepoTakendown" },
+        { "name": "RepoSuspended" },
+        { "name": "RepoDeactivated" }
+      ]
+    }
+  }
+}
diff --git a/lexicons/com/atproto/sync/getCheckout.json b/lexicons/com/atproto/sync/getCheckout.json
new file mode 100644
index 00000000..b3fb375e
--- /dev/null
+++ b/lexicons/com/atproto/sync/getCheckout.json
@@ -0,0 +1,24 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.sync.getCheckout",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "DEPRECATED - please use com.atproto.sync.getRepo instead",
+      "parameters": {
+        "type": "params",
+        "required": ["did"],
+        "properties": {
+          "did": {
+            "type": "string",
+            "format": "did",
+            "description": "The DID of the repo."
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/vnd.ipld.car"
+      }
+    }
+  }
+}
diff --git a/lexicons/com/atproto/sync/getHead.json b/lexicons/com/atproto/sync/getHead.json
new file mode 100644
index 00000000..31ed72cb
--- /dev/null
+++ b/lexicons/com/atproto/sync/getHead.json
@@ -0,0 +1,32 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.sync.getHead",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "DEPRECATED - please use com.atproto.sync.getLatestCommit instead",
+      "parameters": {
+        "type": "params",
+        "required": ["did"],
+        "properties": {
+          "did": {
+            "type": "string",
+            "format": "did",
+            "description": "The DID of the repo."
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["root"],
+          "properties": {
+            "root": { "type": "string", "format": "cid" }
+          }
+        }
+      },
+      "errors": [{ "name": "HeadNotFound" }]
+    }
+  }
+}
diff --git a/lexicons/com/atproto/sync/getHostStatus.json b/lexicons/com/atproto/sync/getHostStatus.json
new file mode 100644
index 00000000..76759f29
--- /dev/null
+++ b/lexicons/com/atproto/sync/getHostStatus.json
@@ -0,0 +1,43 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.sync.getHostStatus",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Returns information about a specified upstream host, as consumed by the server. Implemented by relays.",
+      "parameters": {
+        "type": "params",
+        "required": ["hostname"],
+        "properties": {
+          "hostname": {
+            "type": "string",
+            "description": "Hostname of the host (eg, PDS or relay) being queried."
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["hostname"],
+          "properties": {
+            "hostname": { "type": "string" },
+            "seq": {
+              "type": "integer",
+              "description": "Recent repo stream event sequence number. May be delayed from actual stream processing (eg, persisted cursor not in-memory cursor)."
+            },
+            "accountCount": {
+              "type": "integer",
+              "description": "Number of accounts on the server which are associated with the upstream host. Note that the upstream may actually have more accounts."
+            },
+            "status": {
+              "type": "ref",
+              "ref": "com.atproto.sync.defs#hostStatus"
+            }
+          }
+        }
+      },
+      "errors": [{ "name": "HostNotFound" }]
+    }
+  }
+}
diff --git a/lexicons/com/atproto/sync/getLatestCommit.json b/lexicons/com/atproto/sync/getLatestCommit.json
new file mode 100644
index 00000000..b20e631a
--- /dev/null
+++ b/lexicons/com/atproto/sync/getLatestCommit.json
@@ -0,0 +1,38 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.sync.getLatestCommit",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get the current commit CID & revision of the specified repo. Does not require auth.",
+      "parameters": {
+        "type": "params",
+        "required": ["did"],
+        "properties": {
+          "did": {
+            "type": "string",
+            "format": "did",
+            "description": "The DID of the repo."
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["cid", "rev"],
+          "properties": {
+            "cid": { "type": "string", "format": "cid" },
+            "rev": { "type": "string", "format": "tid" }
+          }
+        }
+      },
+      "errors": [
+        { "name": "RepoNotFound" },
+        { "name": "RepoTakendown" },
+        { "name": "RepoSuspended" },
+        { "name": "RepoDeactivated" }
+      ]
+    }
+  }
+}
diff --git a/lexicons/com/atproto/sync/getRecord.json b/lexicons/com/atproto/sync/getRecord.json
new file mode 100644
index 00000000..ecfc5acc
--- /dev/null
+++ b/lexicons/com/atproto/sync/getRecord.json
@@ -0,0 +1,37 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.sync.getRecord",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get data blocks needed to prove the existence or non-existence of record in the current version of repo. Does not require auth.",
+      "parameters": {
+        "type": "params",
+        "required": ["did", "collection", "rkey"],
+        "properties": {
+          "did": {
+            "type": "string",
+            "format": "did",
+            "description": "The DID of the repo."
+          },
+          "collection": { "type": "string", "format": "nsid" },
+          "rkey": {
+            "type": "string",
+            "description": "Record Key",
+            "format": "record-key"
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/vnd.ipld.car"
+      },
+      "errors": [
+        { "name": "RecordNotFound" },
+        { "name": "RepoNotFound" },
+        { "name": "RepoTakendown" },
+        { "name": "RepoSuspended" },
+        { "name": "RepoDeactivated" }
+      ]
+    }
+  }
+}
diff --git a/lexicons/com/atproto/sync/getRepo.json b/lexicons/com/atproto/sync/getRepo.json
new file mode 100644
index 00000000..307202c2
--- /dev/null
+++ b/lexicons/com/atproto/sync/getRepo.json
@@ -0,0 +1,35 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.sync.getRepo",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Download a repository export as CAR file. Optionally only a 'diff' since a previous revision. Does not require auth; implemented by PDS.",
+      "parameters": {
+        "type": "params",
+        "required": ["did"],
+        "properties": {
+          "did": {
+            "type": "string",
+            "format": "did",
+            "description": "The DID of the repo."
+          },
+          "since": {
+            "type": "string",
+            "format": "tid",
+            "description": "The revision ('rev') of the repo to create a diff from."
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/vnd.ipld.car"
+      },
+      "errors": [
+        { "name": "RepoNotFound" },
+        { "name": "RepoTakendown" },
+        { "name": "RepoSuspended" },
+        { "name": "RepoDeactivated" }
+      ]
+    }
+  }
+}
diff --git a/lexicons/com/atproto/sync/getRepoStatus.json b/lexicons/com/atproto/sync/getRepoStatus.json
new file mode 100644
index 00000000..347a710c
--- /dev/null
+++ b/lexicons/com/atproto/sync/getRepoStatus.json
@@ -0,0 +1,50 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.sync.getRepoStatus",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get the hosting status for a repository, on this server. Expected to be implemented by PDS and Relay.",
+      "parameters": {
+        "type": "params",
+        "required": ["did"],
+        "properties": {
+          "did": {
+            "type": "string",
+            "format": "did",
+            "description": "The DID of the repo."
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["did", "active"],
+          "properties": {
+            "did": { "type": "string", "format": "did" },
+            "active": { "type": "boolean" },
+            "status": {
+              "type": "string",
+              "description": "If active=false, this optional field indicates a possible reason for why the account is not active. If active=false and no status is supplied, then the host makes no claim for why the repository is no longer being hosted.",
+              "knownValues": [
+                "takendown",
+                "suspended",
+                "deleted",
+                "deactivated",
+                "desynchronized",
+                "throttled"
+              ]
+            },
+            "rev": {
+              "type": "string",
+              "format": "tid",
+              "description": "Optional field, the current rev of the repo, if active=true"
+            }
+          }
+        }
+      },
+      "errors": [{ "name": "RepoNotFound" }]
+    }
+  }
+}
diff --git a/lexicons/com/atproto/sync/listBlobs.json b/lexicons/com/atproto/sync/listBlobs.json
new file mode 100644
index 00000000..45a048d1
--- /dev/null
+++ b/lexicons/com/atproto/sync/listBlobs.json
@@ -0,0 +1,53 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.sync.listBlobs",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "List blob CIDs for an account, since some repo revision. Does not require auth; implemented by PDS.",
+      "parameters": {
+        "type": "params",
+        "required": ["did"],
+        "properties": {
+          "did": {
+            "type": "string",
+            "format": "did",
+            "description": "The DID of the repo."
+          },
+          "since": {
+            "type": "string",
+            "format": "tid",
+            "description": "Optional revision of the repo to list blobs since."
+          },
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 1000,
+            "default": 500
+          },
+          "cursor": { "type": "string" }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["cids"],
+          "properties": {
+            "cursor": { "type": "string" },
+            "cids": {
+              "type": "array",
+              "items": { "type": "string", "format": "cid" }
+            }
+          }
+        }
+      },
+      "errors": [
+        { "name": "RepoNotFound" },
+        { "name": "RepoTakendown" },
+        { "name": "RepoSuspended" },
+        { "name": "RepoDeactivated" }
+      ]
+    }
+  }
+}
diff --git a/lexicons/com/atproto/sync/listHosts.json b/lexicons/com/atproto/sync/listHosts.json
new file mode 100644
index 00000000..3ae8e398
--- /dev/null
+++ b/lexicons/com/atproto/sync/listHosts.json
@@ -0,0 +1,56 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.sync.listHosts",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Enumerates upstream hosts (eg, PDS or relay instances) that this service consumes from. Implemented by relays.",
+      "parameters": {
+        "type": "params",
+        "properties": {
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 1000,
+            "default": 200
+          },
+          "cursor": { "type": "string" }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["hosts"],
+          "properties": {
+            "cursor": { "type": "string" },
+            "hosts": {
+              "type": "array",
+              "items": { "type": "ref", "ref": "#host" },
+              "description": "Sort order is not formally specified. Recommended order is by time host was first seen by the server, with oldest first."
+            }
+          }
+        }
+      }
+    },
+    "host": {
+      "type": "object",
+      "required": ["hostname"],
+      "properties": {
+        "hostname": {
+          "type": "string",
+          "description": "hostname of server; not a URL (no scheme)"
+        },
+        "seq": {
+          "type": "integer",
+          "description": "Recent repo stream event sequence number. May be delayed from actual stream processing (eg, persisted cursor not in-memory cursor)."
+        },
+        "accountCount": { "type": "integer" },
+        "status": {
+          "type": "ref",
+          "ref": "com.atproto.sync.defs#hostStatus"
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/com/atproto/sync/listRepos.json b/lexicons/com/atproto/sync/listRepos.json
new file mode 100644
index 00000000..8115bbd6
--- /dev/null
+++ b/lexicons/com/atproto/sync/listRepos.json
@@ -0,0 +1,62 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.sync.listRepos",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Enumerates all the DID, rev, and commit CID for all repos hosted by this service. Does not require auth; implemented by PDS and Relay.",
+      "parameters": {
+        "type": "params",
+        "properties": {
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 1000,
+            "default": 500
+          },
+          "cursor": { "type": "string" }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["repos"],
+          "properties": {
+            "cursor": { "type": "string" },
+            "repos": {
+              "type": "array",
+              "items": { "type": "ref", "ref": "#repo" }
+            }
+          }
+        }
+      }
+    },
+    "repo": {
+      "type": "object",
+      "required": ["did", "head", "rev"],
+      "properties": {
+        "did": { "type": "string", "format": "did" },
+        "head": {
+          "type": "string",
+          "format": "cid",
+          "description": "Current repo commit CID"
+        },
+        "rev": { "type": "string", "format": "tid" },
+        "active": { "type": "boolean" },
+        "status": {
+          "type": "string",
+          "description": "If active=false, this optional field indicates a possible reason for why the account is not active. If active=false and no status is supplied, then the host makes no claim for why the repository is no longer being hosted.",
+          "knownValues": [
+            "takendown",
+            "suspended",
+            "deleted",
+            "deactivated",
+            "desynchronized",
+            "throttled"
+          ]
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/com/atproto/sync/listReposByCollection.json b/lexicons/com/atproto/sync/listReposByCollection.json
new file mode 100644
index 00000000..04027723
--- /dev/null
+++ b/lexicons/com/atproto/sync/listReposByCollection.json
@@ -0,0 +1,46 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.sync.listReposByCollection",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Enumerates all the DIDs which have records with the given collection NSID.",
+      "parameters": {
+        "type": "params",
+        "required": ["collection"],
+        "properties": {
+          "collection": { "type": "string", "format": "nsid" },
+          "limit": {
+            "type": "integer",
+            "description": "Maximum size of response set. Recommend setting a large maximum (1000+) when enumerating large DID lists.",
+            "minimum": 1,
+            "maximum": 2000,
+            "default": 500
+          },
+          "cursor": { "type": "string" }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["repos"],
+          "properties": {
+            "cursor": { "type": "string" },
+            "repos": {
+              "type": "array",
+              "items": { "type": "ref", "ref": "#repo" }
+            }
+          }
+        }
+      }
+    },
+    "repo": {
+      "type": "object",
+      "required": ["did"],
+      "properties": {
+        "did": { "type": "string", "format": "did" }
+      }
+    }
+  }
+}
diff --git a/lexicons/com/atproto/sync/notifyOfUpdate.json b/lexicons/com/atproto/sync/notifyOfUpdate.json
new file mode 100644
index 00000000..63097386
--- /dev/null
+++ b/lexicons/com/atproto/sync/notifyOfUpdate.json
@@ -0,0 +1,23 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.sync.notifyOfUpdate",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Notify a crawling service of a recent update, and that crawling should resume. Intended use is after a gap between repo stream events caused the crawling service to disconnect. Does not require auth; implemented by Relay. DEPRECATED: just use com.atproto.sync.requestCrawl",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["hostname"],
+          "properties": {
+            "hostname": {
+              "type": "string",
+              "description": "Hostname of the current service (usually a PDS) that is notifying of update."
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/com/atproto/sync/requestCrawl.json b/lexicons/com/atproto/sync/requestCrawl.json
new file mode 100644
index 00000000..8f01ebc0
--- /dev/null
+++ b/lexicons/com/atproto/sync/requestCrawl.json
@@ -0,0 +1,24 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.sync.requestCrawl",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Request a service to persistently crawl hosted repos. Expected use is new PDS instances declaring their existence to Relays. Does not require auth.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["hostname"],
+          "properties": {
+            "hostname": {
+              "type": "string",
+              "description": "Hostname of the current service (eg, PDS) that is requesting to be crawled."
+            }
+          }
+        }
+      },
+      "errors": [{ "name": "HostBanned" }]
+    }
+  }
+}
diff --git a/lexicons/com/atproto/sync/subscribeRepos.json b/lexicons/com/atproto/sync/subscribeRepos.json
new file mode 100644
index 00000000..373b050f
--- /dev/null
+++ b/lexicons/com/atproto/sync/subscribeRepos.json
@@ -0,0 +1,215 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.sync.subscribeRepos",
+  "defs": {
+    "main": {
+      "type": "subscription",
+      "description": "Repository event stream, aka Firehose endpoint. Outputs repo commits with diff data, and identity update events, for all repositories on the current server. See the atproto specifications for details around stream sequencing, repo versioning, CAR diff format, and more. Public and does not require auth; implemented by PDS and Relay.",
+      "parameters": {
+        "type": "params",
+        "properties": {
+          "cursor": {
+            "type": "integer",
+            "description": "The last known event seq number to backfill from."
+          }
+        }
+      },
+      "message": {
+        "schema": {
+          "type": "union",
+          "refs": ["#commit", "#sync", "#identity", "#account", "#info"]
+        }
+      },
+      "errors": [
+        { "name": "FutureCursor" },
+        {
+          "name": "ConsumerTooSlow",
+          "description": "If the consumer of the stream can not keep up with events, and a backlog gets too large, the server will drop the connection."
+        }
+      ]
+    },
+    "commit": {
+      "type": "object",
+      "description": "Represents an update of repository state. Note that empty commits are allowed, which include no repo data changes, but an update to rev and signature.",
+      "required": [
+        "seq",
+        "rebase",
+        "tooBig",
+        "repo",
+        "commit",
+        "rev",
+        "since",
+        "blocks",
+        "ops",
+        "blobs",
+        "time"
+      ],
+      "nullable": ["since"],
+      "properties": {
+        "seq": {
+          "type": "integer",
+          "description": "The stream sequence number of this message."
+        },
+        "rebase": { "type": "boolean", "description": "DEPRECATED -- unused" },
+        "tooBig": {
+          "type": "boolean",
+          "description": "DEPRECATED -- replaced by #sync event and data limits. Indicates that this commit contained too many ops, or data size was too large. Consumers will need to make a separate request to get missing data."
+        },
+        "repo": {
+          "type": "string",
+          "format": "did",
+          "description": "The repo this event comes from. Note that all other message types name this field 'did'."
+        },
+        "commit": {
+          "type": "cid-link",
+          "description": "Repo commit object CID."
+        },
+        "rev": {
+          "type": "string",
+          "format": "tid",
+          "description": "The rev of the emitted commit. Note that this information is also in the commit object included in blocks, unless this is a tooBig event."
+        },
+        "since": {
+          "type": "string",
+          "format": "tid",
+          "description": "The rev of the last emitted commit from this repo (if any)."
+        },
+        "blocks": {
+          "type": "bytes",
+          "description": "CAR file containing relevant blocks, as a diff since the previous repo state. The commit must be included as a block, and the commit block CID must be the first entry in the CAR header 'roots' list.",
+          "maxLength": 2000000
+        },
+        "ops": {
+          "type": "array",
+          "items": {
+            "type": "ref",
+            "ref": "#repoOp",
+            "description": "List of repo mutation operations in this commit (eg, records created, updated, or deleted)."
+          },
+          "maxLength": 200
+        },
+        "blobs": {
+          "type": "array",
+          "items": {
+            "type": "cid-link",
+            "description": "DEPRECATED -- will soon always be empty. List of new blobs (by CID) referenced by records in this commit."
+          }
+        },
+        "prevData": {
+          "type": "cid-link",
+          "description": "The root CID of the MST tree for the previous commit from this repo (indicated by the 'since' revision field in this message). Corresponds to the 'data' field in the repo commit object. NOTE: this field is effectively required for the 'inductive' version of firehose."
+        },
+        "time": {
+          "type": "string",
+          "format": "datetime",
+          "description": "Timestamp of when this message was originally broadcast."
+        }
+      }
+    },
+    "sync": {
+      "type": "object",
+      "description": "Updates the repo to a new state, without necessarily including that state on the firehose. Used to recover from broken commit streams, data loss incidents, or in situations where upstream host does not know recent state of the repository.",
+      "required": ["seq", "did", "blocks", "rev", "time"],
+      "properties": {
+        "seq": {
+          "type": "integer",
+          "description": "The stream sequence number of this message."
+        },
+        "did": {
+          "type": "string",
+          "format": "did",
+          "description": "The account this repo event corresponds to. Must match that in the commit object."
+        },
+        "blocks": {
+          "type": "bytes",
+          "description": "CAR file containing the commit, as a block. The CAR header must include the commit block CID as the first 'root'.",
+          "maxLength": 10000
+        },
+        "rev": {
+          "type": "string",
+          "description": "The rev of the commit. This value must match that in the commit object."
+        },
+        "time": {
+          "type": "string",
+          "format": "datetime",
+          "description": "Timestamp of when this message was originally broadcast."
+        }
+      }
+    },
+    "identity": {
+      "type": "object",
+      "description": "Represents a change to an account's identity. Could be an updated handle, signing key, or pds hosting endpoint. Serves as a prod to all downstream services to refresh their identity cache.",
+      "required": ["seq", "did", "time"],
+      "properties": {
+        "seq": { "type": "integer" },
+        "did": { "type": "string", "format": "did" },
+        "time": { "type": "string", "format": "datetime" },
+        "handle": {
+          "type": "string",
+          "format": "handle",
+          "description": "The current handle for the account, or 'handle.invalid' if validation fails. This field is optional, might have been validated or passed-through from an upstream source. Semantics and behaviors for PDS vs Relay may evolve in the future; see atproto specs for more details."
+        }
+      }
+    },
+    "account": {
+      "type": "object",
+      "description": "Represents a change to an account's status on a host (eg, PDS or Relay). The semantics of this event are that the status is at the host which emitted the event, not necessarily that at the currently active PDS. Eg, a Relay takedown would emit a takedown with active=false, even if the PDS is still active.",
+      "required": ["seq", "did", "time", "active"],
+      "properties": {
+        "seq": { "type": "integer" },
+        "did": { "type": "string", "format": "did" },
+        "time": { "type": "string", "format": "datetime" },
+        "active": {
+          "type": "boolean",
+          "description": "Indicates that the account has a repository which can be fetched from the host that emitted this event."
+        },
+        "status": {
+          "type": "string",
+          "description": "If active=false, this optional field indicates a reason for why the account is not active.",
+          "knownValues": [
+            "takendown",
+            "suspended",
+            "deleted",
+            "deactivated",
+            "desynchronized",
+            "throttled"
+          ]
+        }
+      }
+    },
+    "info": {
+      "type": "object",
+      "required": ["name"],
+      "properties": {
+        "name": {
+          "type": "string",
+          "knownValues": ["OutdatedCursor"]
+        },
+        "message": {
+          "type": "string"
+        }
+      }
+    },
+    "repoOp": {
+      "type": "object",
+      "description": "A repo operation, ie a mutation of a single record.",
+      "required": ["action", "path", "cid"],
+      "nullable": ["cid"],
+      "properties": {
+        "action": {
+          "type": "string",
+          "knownValues": ["create", "update", "delete"]
+        },
+        "path": { "type": "string" },
+        "cid": {
+          "type": "cid-link",
+          "description": "For creates and updates, the new record CID. For deletions, null."
+        },
+        "prev": {
+          "type": "cid-link",
+          "description": "For updates and deletes, the previous record CID (required for inductive firehose). For creations, field should not be defined."
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/com/atproto/temp/addReservedHandle.json b/lexicons/com/atproto/temp/addReservedHandle.json
new file mode 100644
index 00000000..a8b1aac9
--- /dev/null
+++ b/lexicons/com/atproto/temp/addReservedHandle.json
@@ -0,0 +1,27 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.temp.addReservedHandle",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Add a handle to the set of reserved handles.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["handle"],
+          "properties": {
+            "handle": { "type": "string" }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "properties": {}
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/com/atproto/temp/checkHandleAvailability.json b/lexicons/com/atproto/temp/checkHandleAvailability.json
new file mode 100644
index 00000000..ffbdd6f7
--- /dev/null
+++ b/lexicons/com/atproto/temp/checkHandleAvailability.json
@@ -0,0 +1,88 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.temp.checkHandleAvailability",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Checks whether the provided handle is available. If the handle is not available, available suggestions will be returned. Optional inputs will be used to generate suggestions.",
+      "parameters": {
+        "type": "params",
+        "required": ["handle"],
+        "properties": {
+          "handle": {
+            "type": "string",
+            "format": "handle",
+            "description": "Tentative handle. Will be checked for availability or used to build handle suggestions."
+          },
+          "email": {
+            "type": "string",
+            "description": "User-provided email. Might be used to build handle suggestions."
+          },
+          "birthDate": {
+            "type": "string",
+            "format": "datetime",
+            "description": "User-provided birth date. Might be used to build handle suggestions."
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["handle", "result"],
+          "properties": {
+            "handle": {
+              "type": "string",
+              "format": "handle",
+              "description": "Echo of the input handle."
+            },
+            "result": {
+              "type": "union",
+              "refs": ["#resultAvailable", "#resultUnavailable"]
+            }
+          }
+        }
+      },
+      "errors": [
+        {
+          "name": "InvalidEmail",
+          "description": "An invalid email was provided."
+        }
+      ]
+    },
+    "resultAvailable": {
+      "type": "object",
+      "description": "Indicates the provided handle is available.",
+      "properties": {}
+    },
+    "resultUnavailable": {
+      "type": "object",
+      "description": "Indicates the provided handle is unavailable and gives suggestions of available handles.",
+      "required": ["suggestions"],
+      "properties": {
+        "suggestions": {
+          "type": "array",
+          "description": "List of suggested handles based on the provided inputs.",
+          "items": {
+            "type": "ref",
+            "ref": "#suggestion"
+          }
+        }
+      }
+    },
+    "suggestion": {
+      "type": "object",
+      "required": ["handle", "method"],
+      "properties": {
+        "handle": {
+          "type": "string",
+          "format": "handle"
+        },
+        "method": {
+          "type": "string",
+          "description": "Method used to build this suggestion. Should be considered opaque to clients. Can be used for metrics."
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/com/atproto/temp/checkSignupQueue.json b/lexicons/com/atproto/temp/checkSignupQueue.json
new file mode 100644
index 00000000..b7d9e65d
--- /dev/null
+++ b/lexicons/com/atproto/temp/checkSignupQueue.json
@@ -0,0 +1,22 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.temp.checkSignupQueue",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Check accounts location in signup queue.",
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["activated"],
+          "properties": {
+            "activated": { "type": "boolean" },
+            "placeInQueue": { "type": "integer" },
+            "estimatedTimeMs": { "type": "integer" }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/com/atproto/temp/dereferenceScope.json b/lexicons/com/atproto/temp/dereferenceScope.json
new file mode 100644
index 00000000..548dfb9c
--- /dev/null
+++ b/lexicons/com/atproto/temp/dereferenceScope.json
@@ -0,0 +1,39 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.temp.dereferenceScope",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Allows finding the oauth permission scope from a reference",
+      "parameters": {
+        "type": "params",
+        "required": ["scope"],
+        "properties": {
+          "scope": {
+            "type": "string",
+            "description": "The scope reference (starts with 'ref:')"
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["scope"],
+          "properties": {
+            "scope": {
+              "type": "string",
+              "description": "The full oauth permission scope"
+            }
+          }
+        }
+      },
+      "errors": [
+        {
+          "name": "InvalidScopeReference",
+          "description": "An invalid scope reference was provided."
+        }
+      ]
+    }
+  }
+}
diff --git a/lexicons/com/atproto/temp/fetchLabels.json b/lexicons/com/atproto/temp/fetchLabels.json
new file mode 100644
index 00000000..57c3f732
--- /dev/null
+++ b/lexicons/com/atproto/temp/fetchLabels.json
@@ -0,0 +1,35 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.temp.fetchLabels",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "DEPRECATED: use queryLabels or subscribeLabels instead -- Fetch all labels from a labeler created after a certain date.",
+      "parameters": {
+        "type": "params",
+        "properties": {
+          "since": { "type": "integer" },
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 250,
+            "default": 50
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["labels"],
+          "properties": {
+            "labels": {
+              "type": "array",
+              "items": { "type": "ref", "ref": "com.atproto.label.defs#label" }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/com/atproto/temp/requestPhoneVerification.json b/lexicons/com/atproto/temp/requestPhoneVerification.json
new file mode 100644
index 00000000..56beeb81
--- /dev/null
+++ b/lexicons/com/atproto/temp/requestPhoneVerification.json
@@ -0,0 +1,20 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.temp.requestPhoneVerification",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Request a verification code to be sent to the supplied phone number",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["phoneNumber"],
+          "properties": {
+            "phoneNumber": { "type": "string" }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/com/atproto/temp/revokeAccountCredentials.json b/lexicons/com/atproto/temp/revokeAccountCredentials.json
new file mode 100644
index 00000000..9d23a12b
--- /dev/null
+++ b/lexicons/com/atproto/temp/revokeAccountCredentials.json
@@ -0,0 +1,23 @@
+{
+  "lexicon": 1,
+  "id": "com.atproto.temp.revokeAccountCredentials",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Revoke sessions, password, and app passwords associated with account. May be resolved by a password reset.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["account"],
+          "properties": {
+            "account": {
+              "type": "string",
+              "format": "at-identifier"
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/tools/ozone/communication/createTemplate.json b/lexicons/tools/ozone/communication/createTemplate.json
new file mode 100644
index 00000000..902a4644
--- /dev/null
+++ b/lexicons/tools/ozone/communication/createTemplate.json
@@ -0,0 +1,49 @@
+{
+  "lexicon": 1,
+  "id": "tools.ozone.communication.createTemplate",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Administrative action to create a new, re-usable communication (email for now) template.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["subject", "contentMarkdown", "name"],
+          "properties": {
+            "name": {
+              "type": "string",
+              "description": "Name of the template."
+            },
+            "contentMarkdown": {
+              "type": "string",
+              "description": "Content of the template, markdown supported, can contain variable placeholders."
+            },
+            "subject": {
+              "type": "string",
+              "description": "Subject of the message, used in emails."
+            },
+            "lang": {
+              "type": "string",
+              "format": "language",
+              "description": "Message language."
+            },
+            "createdBy": {
+              "type": "string",
+              "format": "did",
+              "description": "DID of the user who is creating the template."
+            }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "ref",
+          "ref": "tools.ozone.communication.defs#templateView"
+        }
+      },
+      "errors": [{ "name": "DuplicateTemplateName" }]
+    }
+  }
+}
diff --git a/lexicons/tools/ozone/communication/defs.json b/lexicons/tools/ozone/communication/defs.json
new file mode 100644
index 00000000..fe611569
--- /dev/null
+++ b/lexicons/tools/ozone/communication/defs.json
@@ -0,0 +1,43 @@
+{
+  "lexicon": 1,
+  "id": "tools.ozone.communication.defs",
+  "defs": {
+    "templateView": {
+      "type": "object",
+      "required": [
+        "id",
+        "name",
+        "contentMarkdown",
+        "disabled",
+        "lastUpdatedBy",
+        "createdAt",
+        "updatedAt"
+      ],
+      "properties": {
+        "id": { "type": "string" },
+        "name": { "type": "string", "description": "Name of the template." },
+        "subject": {
+          "type": "string",
+          "description": "Content of the template, can contain markdown and variable placeholders."
+        },
+        "contentMarkdown": {
+          "type": "string",
+          "description": "Subject of the message, used in emails."
+        },
+        "disabled": { "type": "boolean" },
+        "lang": {
+          "type": "string",
+          "format": "language",
+          "description": "Message language."
+        },
+        "lastUpdatedBy": {
+          "type": "string",
+          "format": "did",
+          "description": "DID of the user who last updated the template."
+        },
+        "createdAt": { "type": "string", "format": "datetime" },
+        "updatedAt": { "type": "string", "format": "datetime" }
+      }
+    }
+  }
+}
diff --git a/lexicons/tools/ozone/communication/deleteTemplate.json b/lexicons/tools/ozone/communication/deleteTemplate.json
new file mode 100644
index 00000000..c85bd349
--- /dev/null
+++ b/lexicons/tools/ozone/communication/deleteTemplate.json
@@ -0,0 +1,20 @@
+{
+  "lexicon": 1,
+  "id": "tools.ozone.communication.deleteTemplate",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Delete a communication template.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["id"],
+          "properties": {
+            "id": { "type": "string" }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/tools/ozone/communication/listTemplates.json b/lexicons/tools/ozone/communication/listTemplates.json
new file mode 100644
index 00000000..65a24ed7
--- /dev/null
+++ b/lexicons/tools/ozone/communication/listTemplates.json
@@ -0,0 +1,26 @@
+{
+  "lexicon": 1,
+  "id": "tools.ozone.communication.listTemplates",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get list of all communication templates.",
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["communicationTemplates"],
+          "properties": {
+            "communicationTemplates": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "tools.ozone.communication.defs#templateView"
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/tools/ozone/communication/updateTemplate.json b/lexicons/tools/ozone/communication/updateTemplate.json
new file mode 100644
index 00000000..fc2b3f68
--- /dev/null
+++ b/lexicons/tools/ozone/communication/updateTemplate.json
@@ -0,0 +1,56 @@
+{
+  "lexicon": 1,
+  "id": "tools.ozone.communication.updateTemplate",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Administrative action to update an existing communication template. Allows passing partial fields to patch specific fields only.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["id"],
+          "properties": {
+            "id": {
+              "type": "string",
+              "description": "ID of the template to be updated."
+            },
+            "name": {
+              "type": "string",
+              "description": "Name of the template."
+            },
+            "lang": {
+              "type": "string",
+              "format": "language",
+              "description": "Message language."
+            },
+            "contentMarkdown": {
+              "type": "string",
+              "description": "Content of the template, markdown supported, can contain variable placeholders."
+            },
+            "subject": {
+              "type": "string",
+              "description": "Subject of the message, used in emails."
+            },
+            "updatedBy": {
+              "type": "string",
+              "format": "did",
+              "description": "DID of the user who is updating the template."
+            },
+            "disabled": {
+              "type": "boolean"
+            }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "ref",
+          "ref": "tools.ozone.communication.defs#templateView"
+        }
+      },
+      "errors": [{ "name": "DuplicateTemplateName" }]
+    }
+  }
+}
diff --git a/lexicons/tools/ozone/hosting/getAccountHistory.json b/lexicons/tools/ozone/hosting/getAccountHistory.json
new file mode 100644
index 00000000..27cb8289
--- /dev/null
+++ b/lexicons/tools/ozone/hosting/getAccountHistory.json
@@ -0,0 +1,106 @@
+{
+  "lexicon": 1,
+  "id": "tools.ozone.hosting.getAccountHistory",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get account history, e.g. log of updated email addresses or other identity information.",
+      "parameters": {
+        "type": "params",
+        "required": ["did"],
+        "properties": {
+          "did": { "type": "string", "format": "did" },
+          "events": {
+            "type": "array",
+            "items": {
+              "type": "string",
+              "knownValues": [
+                "accountCreated",
+                "emailUpdated",
+                "emailConfirmed",
+                "passwordUpdated",
+                "handleUpdated"
+              ]
+            }
+          },
+          "cursor": { "type": "string" },
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 100,
+            "default": 50
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["events"],
+          "properties": {
+            "cursor": { "type": "string" },
+            "events": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "#event"
+              }
+            }
+          }
+        }
+      }
+    },
+    "event": {
+      "type": "object",
+      "required": ["details", "createdBy", "createdAt"],
+      "properties": {
+        "details": {
+          "type": "union",
+          "refs": [
+            "#accountCreated",
+            "#emailUpdated",
+            "#emailConfirmed",
+            "#passwordUpdated",
+            "#handleUpdated"
+          ]
+        },
+        "createdBy": { "type": "string" },
+        "createdAt": { "type": "string", "format": "datetime" }
+      }
+    },
+    "accountCreated": {
+      "type": "object",
+      "required": [],
+      "properties": {
+        "email": { "type": "string" },
+        "handle": { "type": "string", "format": "handle" }
+      }
+    },
+    "emailUpdated": {
+      "type": "object",
+      "required": ["email"],
+      "properties": {
+        "email": { "type": "string" }
+      }
+    },
+    "emailConfirmed": {
+      "type": "object",
+      "required": ["email"],
+      "properties": {
+        "email": { "type": "string" }
+      }
+    },
+    "passwordUpdated": {
+      "type": "object",
+      "required": [],
+      "properties": {}
+    },
+    "handleUpdated": {
+      "type": "object",
+      "required": ["handle"],
+      "properties": {
+        "handle": { "type": "string", "format": "handle" }
+      }
+    }
+  }
+}
diff --git a/lexicons/tools/ozone/moderation/cancelScheduledActions.json b/lexicons/tools/ozone/moderation/cancelScheduledActions.json
new file mode 100644
index 00000000..10abebeb
--- /dev/null
+++ b/lexicons/tools/ozone/moderation/cancelScheduledActions.json
@@ -0,0 +1,64 @@
+{
+  "lexicon": 1,
+  "id": "tools.ozone.moderation.cancelScheduledActions",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Cancel all pending scheduled moderation actions for specified subjects",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["subjects"],
+          "properties": {
+            "subjects": {
+              "type": "array",
+              "maxLength": 100,
+              "items": {
+                "type": "string",
+                "format": "did"
+              },
+              "description": "Array of DID subjects to cancel scheduled actions for"
+            },
+            "comment": {
+              "type": "string",
+              "description": "Optional comment describing the reason for cancellation"
+            }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "ref",
+          "ref": "#cancellationResults"
+        }
+      }
+    },
+    "cancellationResults": {
+      "type": "object",
+      "required": ["succeeded", "failed"],
+      "properties": {
+        "succeeded": {
+          "type": "array",
+          "items": { "type": "string", "format": "did" },
+          "description": "DIDs for which all pending scheduled actions were successfully cancelled"
+        },
+        "failed": {
+          "type": "array",
+          "items": { "type": "ref", "ref": "#failedCancellation" },
+          "description": "DIDs for which cancellation failed with error details"
+        }
+      }
+    },
+    "failedCancellation": {
+      "type": "object",
+      "required": ["did", "error"],
+      "properties": {
+        "did": { "type": "string", "format": "did" },
+        "error": { "type": "string" },
+        "errorCode": { "type": "string" }
+      }
+    }
+  }
+}
diff --git a/lexicons/tools/ozone/moderation/defs.json b/lexicons/tools/ozone/moderation/defs.json
new file mode 100644
index 00000000..d5908562
--- /dev/null
+++ b/lexicons/tools/ozone/moderation/defs.json
@@ -0,0 +1,1206 @@
+{
+  "lexicon": 1,
+  "id": "tools.ozone.moderation.defs",
+  "defs": {
+    "modEventView": {
+      "type": "object",
+      "required": [
+        "id",
+        "event",
+        "subject",
+        "subjectBlobCids",
+        "createdBy",
+        "createdAt"
+      ],
+      "properties": {
+        "id": { "type": "integer" },
+        "event": {
+          "type": "union",
+          "refs": [
+            "#modEventTakedown",
+            "#modEventReverseTakedown",
+            "#modEventComment",
+            "#modEventReport",
+            "#modEventLabel",
+            "#modEventAcknowledge",
+            "#modEventEscalate",
+            "#modEventMute",
+            "#modEventUnmute",
+            "#modEventMuteReporter",
+            "#modEventUnmuteReporter",
+            "#modEventEmail",
+            "#modEventResolveAppeal",
+            "#modEventDivert",
+            "#modEventTag",
+            "#accountEvent",
+            "#identityEvent",
+            "#recordEvent",
+            "#modEventPriorityScore",
+            "#ageAssuranceEvent",
+            "#ageAssuranceOverrideEvent",
+            "#revokeAccountCredentialsEvent",
+            "#scheduleTakedownEvent",
+            "#cancelScheduledTakedownEvent"
+          ]
+        },
+        "subject": {
+          "type": "union",
+          "refs": [
+            "com.atproto.admin.defs#repoRef",
+            "com.atproto.repo.strongRef",
+            "chat.bsky.convo.defs#messageRef"
+          ]
+        },
+        "subjectBlobCids": { "type": "array", "items": { "type": "string" } },
+        "createdBy": { "type": "string", "format": "did" },
+        "createdAt": { "type": "string", "format": "datetime" },
+        "creatorHandle": { "type": "string" },
+        "subjectHandle": { "type": "string" },
+        "modTool": { "type": "ref", "ref": "#modTool" }
+      }
+    },
+    "modEventViewDetail": {
+      "type": "object",
+      "required": [
+        "id",
+        "event",
+        "subject",
+        "subjectBlobs",
+        "createdBy",
+        "createdAt"
+      ],
+      "properties": {
+        "id": { "type": "integer" },
+        "event": {
+          "type": "union",
+          "refs": [
+            "#modEventTakedown",
+            "#modEventReverseTakedown",
+            "#modEventComment",
+            "#modEventReport",
+            "#modEventLabel",
+            "#modEventAcknowledge",
+            "#modEventEscalate",
+            "#modEventMute",
+            "#modEventUnmute",
+            "#modEventMuteReporter",
+            "#modEventUnmuteReporter",
+            "#modEventEmail",
+            "#modEventResolveAppeal",
+            "#modEventDivert",
+            "#modEventTag",
+            "#accountEvent",
+            "#identityEvent",
+            "#recordEvent",
+            "#modEventPriorityScore",
+            "#ageAssuranceEvent",
+            "#ageAssuranceOverrideEvent",
+            "#revokeAccountCredentialsEvent",
+            "#scheduleTakedownEvent",
+            "#cancelScheduledTakedownEvent"
+          ]
+        },
+        "subject": {
+          "type": "union",
+          "refs": [
+            "#repoView",
+            "#repoViewNotFound",
+            "#recordView",
+            "#recordViewNotFound"
+          ]
+        },
+        "subjectBlobs": {
+          "type": "array",
+          "items": { "type": "ref", "ref": "#blobView" }
+        },
+        "createdBy": { "type": "string", "format": "did" },
+        "createdAt": { "type": "string", "format": "datetime" },
+        "modTool": { "type": "ref", "ref": "#modTool" }
+      }
+    },
+    "subjectStatusView": {
+      "type": "object",
+      "required": ["id", "subject", "createdAt", "updatedAt", "reviewState"],
+      "properties": {
+        "id": { "type": "integer" },
+        "subject": {
+          "type": "union",
+          "refs": [
+            "com.atproto.admin.defs#repoRef",
+            "com.atproto.repo.strongRef",
+            "chat.bsky.convo.defs#messageRef"
+          ]
+        },
+        "hosting": {
+          "type": "union",
+          "refs": ["#accountHosting", "#recordHosting"]
+        },
+        "subjectBlobCids": {
+          "type": "array",
+          "items": { "type": "string", "format": "cid" }
+        },
+        "subjectRepoHandle": { "type": "string" },
+        "updatedAt": {
+          "type": "string",
+          "format": "datetime",
+          "description": "Timestamp referencing when the last update was made to the moderation status of the subject"
+        },
+        "createdAt": {
+          "type": "string",
+          "format": "datetime",
+          "description": "Timestamp referencing the first moderation status impacting event was emitted on the subject"
+        },
+        "reviewState": {
+          "type": "ref",
+          "ref": "#subjectReviewState"
+        },
+        "comment": {
+          "type": "string",
+          "description": "Sticky comment on the subject."
+        },
+        "priorityScore": {
+          "type": "integer",
+          "description": "Numeric value representing the level of priority. Higher score means higher priority.",
+          "minimum": 0,
+          "maximum": 100
+        },
+        "muteUntil": {
+          "type": "string",
+          "format": "datetime"
+        },
+        "muteReportingUntil": {
+          "type": "string",
+          "format": "datetime"
+        },
+        "lastReviewedBy": {
+          "type": "string",
+          "format": "did"
+        },
+        "lastReviewedAt": {
+          "type": "string",
+          "format": "datetime"
+        },
+        "lastReportedAt": {
+          "type": "string",
+          "format": "datetime"
+        },
+        "lastAppealedAt": {
+          "type": "string",
+          "format": "datetime",
+          "description": "Timestamp referencing when the author of the subject appealed a moderation action"
+        },
+        "takendown": {
+          "type": "boolean"
+        },
+        "appealed": {
+          "type": "boolean",
+          "description": "True indicates that the a previously taken moderator action was appealed against, by the author of the content. False indicates last appeal was resolved by moderators."
+        },
+        "suspendUntil": {
+          "type": "string",
+          "format": "datetime"
+        },
+        "tags": {
+          "type": "array",
+          "items": { "type": "string" }
+        },
+        "accountStats": {
+          "description": "Statistics related to the account subject",
+          "type": "ref",
+          "ref": "#accountStats"
+        },
+        "recordsStats": {
+          "description": "Statistics related to the record subjects authored by the subject's account",
+          "type": "ref",
+          "ref": "#recordsStats"
+        },
+        "accountStrike": {
+          "description": "Strike information for the account (account-level only)",
+          "type": "ref",
+          "ref": "#accountStrike"
+        },
+        "ageAssuranceState": {
+          "type": "string",
+          "description": "Current age assurance state of the subject.",
+          "knownValues": ["pending", "assured", "unknown", "reset", "blocked"]
+        },
+        "ageAssuranceUpdatedBy": {
+          "type": "string",
+          "description": "Whether or not the last successful update to age assurance was made by the user or admin.",
+          "knownValues": ["admin", "user"]
+        }
+      }
+    },
+    "subjectView": {
+      "description": "Detailed view of a subject. For record subjects, the author's repo and profile will be returned.",
+      "type": "object",
+      "required": ["type", "subject"],
+      "properties": {
+        "type": {
+          "type": "ref",
+          "ref": "com.atproto.moderation.defs#subjectType"
+        },
+        "subject": {
+          "type": "string"
+        },
+        "status": {
+          "type": "ref",
+          "ref": "#subjectStatusView"
+        },
+        "repo": {
+          "type": "ref",
+          "ref": "#repoViewDetail"
+        },
+        "profile": {
+          "type": "union",
+          "refs": []
+        },
+        "record": {
+          "type": "ref",
+          "ref": "#recordViewDetail"
+        }
+      }
+    },
+    "accountStats": {
+      "description": "Statistics about a particular account subject",
+      "type": "object",
+      "properties": {
+        "reportCount": {
+          "description": "Total number of reports on the account",
+          "type": "integer"
+        },
+        "appealCount": {
+          "description": "Total number of appeals against a moderation action on the account",
+          "type": "integer"
+        },
+        "suspendCount": {
+          "description": "Number of times the account was suspended",
+          "type": "integer"
+        },
+        "escalateCount": {
+          "description": "Number of times the account was escalated",
+          "type": "integer"
+        },
+        "takedownCount": {
+          "description": "Number of times the account was taken down",
+          "type": "integer"
+        }
+      }
+    },
+    "recordsStats": {
+      "description": "Statistics about a set of record subject items",
+      "type": "object",
+      "properties": {
+        "totalReports": {
+          "description": "Cumulative sum of the number of reports on the items in the set",
+          "type": "integer"
+        },
+        "reportedCount": {
+          "description": "Number of items that were reported at least once",
+          "type": "integer"
+        },
+        "escalatedCount": {
+          "description": "Number of items that were escalated at least once",
+          "type": "integer"
+        },
+        "appealedCount": {
+          "description": "Number of items that were appealed at least once",
+          "type": "integer"
+        },
+        "subjectCount": {
+          "description": "Total number of item in the set",
+          "type": "integer"
+        },
+        "pendingCount": {
+          "description": "Number of item currently in \"reviewOpen\" or \"reviewEscalated\" state",
+          "type": "integer"
+        },
+        "processedCount": {
+          "description": "Number of item currently in \"reviewNone\" or \"reviewClosed\" state",
+          "type": "integer"
+        },
+        "takendownCount": {
+          "description": "Number of item currently taken down",
+          "type": "integer"
+        }
+      }
+    },
+    "accountStrike": {
+      "description": "Strike information for an account",
+      "type": "object",
+      "properties": {
+        "activeStrikeCount": {
+          "description": "Current number of active strikes (excluding expired strikes)",
+          "type": "integer"
+        },
+        "totalStrikeCount": {
+          "description": "Total number of strikes ever received (including expired strikes)",
+          "type": "integer"
+        },
+        "firstStrikeAt": {
+          "description": "Timestamp of the first strike received",
+          "type": "string",
+          "format": "datetime"
+        },
+        "lastStrikeAt": {
+          "description": "Timestamp of the most recent strike received",
+          "type": "string",
+          "format": "datetime"
+        }
+      }
+    },
+    "subjectReviewState": {
+      "type": "string",
+      "knownValues": [
+        "#reviewOpen",
+        "#reviewEscalated",
+        "#reviewClosed",
+        "#reviewNone"
+      ]
+    },
+    "reviewOpen": {
+      "type": "token",
+      "description": "Moderator review status of a subject: Open. Indicates that the subject needs to be reviewed by a moderator"
+    },
+    "reviewEscalated": {
+      "type": "token",
+      "description": "Moderator review status of a subject: Escalated. Indicates that the subject was escalated for review by a moderator"
+    },
+    "reviewClosed": {
+      "type": "token",
+      "description": "Moderator review status of a subject: Closed. Indicates that the subject was already reviewed and resolved by a moderator"
+    },
+    "reviewNone": {
+      "type": "token",
+      "description": "Moderator review status of a subject: Unnecessary. Indicates that the subject does not need a review at the moment but there is probably some moderation related metadata available for it"
+    },
+    "modEventTakedown": {
+      "type": "object",
+      "description": "Take down a subject permanently or temporarily",
+      "properties": {
+        "comment": {
+          "type": "string"
+        },
+        "durationInHours": {
+          "type": "integer",
+          "description": "Indicates how long the takedown should be in effect before automatically expiring."
+        },
+        "acknowledgeAccountSubjects": {
+          "type": "boolean",
+          "description": "If true, all other reports on content authored by this account will be resolved (acknowledged)."
+        },
+        "policies": {
+          "type": "array",
+          "maxLength": 5,
+          "items": { "type": "string" },
+          "description": "Names/Keywords of the policies that drove the decision."
+        },
+        "severityLevel": {
+          "type": "string",
+          "description": "Severity level of the violation (e.g., 'sev-0', 'sev-1', 'sev-2', etc.)."
+        },
+        "targetServices": {
+          "type": "array",
+          "items": { "type": "string", "knownValues": ["appview", "pds"] },
+          "description": "List of services where the takedown should be applied. If empty or not provided, takedown is applied on all configured services."
+        },
+        "strikeCount": {
+          "type": "integer",
+          "description": "Number of strikes to assign to the user for this violation."
+        },
+        "strikeExpiresAt": {
+          "type": "string",
+          "format": "datetime",
+          "description": "When the strike should expire. If not provided, the strike never expires."
+        }
+      }
+    },
+    "modEventReverseTakedown": {
+      "type": "object",
+      "description": "Revert take down action on a subject",
+      "properties": {
+        "comment": {
+          "type": "string",
+          "description": "Describe reasoning behind the reversal."
+        },
+        "policies": {
+          "type": "array",
+          "maxLength": 5,
+          "items": { "type": "string" },
+          "description": "Names/Keywords of the policy infraction for which takedown is being reversed."
+        },
+        "severityLevel": {
+          "type": "string",
+          "description": "Severity level of the violation. Usually set from the last policy infraction's severity."
+        },
+        "strikeCount": {
+          "type": "integer",
+          "description": "Number of strikes to subtract from the user's strike count. Usually set from the last policy infraction's severity."
+        }
+      }
+    },
+    "modEventResolveAppeal": {
+      "type": "object",
+      "description": "Resolve appeal on a subject",
+      "properties": {
+        "comment": {
+          "type": "string",
+          "description": "Describe resolution."
+        }
+      }
+    },
+    "modEventComment": {
+      "type": "object",
+      "description": "Add a comment to a subject. An empty comment will clear any previously set sticky comment.",
+      "properties": {
+        "comment": {
+          "type": "string"
+        },
+        "sticky": {
+          "type": "boolean",
+          "description": "Make the comment persistent on the subject"
+        }
+      }
+    },
+    "modEventReport": {
+      "type": "object",
+      "description": "Report a subject",
+      "required": ["reportType"],
+      "properties": {
+        "comment": {
+          "type": "string"
+        },
+        "isReporterMuted": {
+          "type": "boolean",
+          "description": "Set to true if the reporter was muted from reporting at the time of the event. These reports won't impact the reviewState of the subject."
+        },
+        "reportType": {
+          "type": "ref",
+          "ref": "com.atproto.moderation.defs#reasonType"
+        }
+      }
+    },
+    "modEventLabel": {
+      "type": "object",
+      "description": "Apply/Negate labels on a subject",
+      "required": ["createLabelVals", "negateLabelVals"],
+      "properties": {
+        "comment": {
+          "type": "string"
+        },
+        "createLabelVals": {
+          "type": "array",
+          "items": { "type": "string" }
+        },
+        "negateLabelVals": {
+          "type": "array",
+          "items": { "type": "string" }
+        },
+        "durationInHours": {
+          "type": "integer",
+          "description": "Indicates how long the label will remain on the subject. Only applies on labels that are being added."
+        }
+      }
+    },
+    "modEventPriorityScore": {
+      "type": "object",
+      "description": "Set priority score of the subject. Higher score means higher priority.",
+      "required": ["score"],
+      "properties": {
+        "comment": {
+          "type": "string"
+        },
+        "score": {
+          "type": "integer",
+          "minimum": 0,
+          "maximum": 100
+        }
+      }
+    },
+    "ageAssuranceEvent": {
+      "type": "object",
+      "description": "Age assurance info coming directly from users. Only works on DID subjects.",
+      "required": ["createdAt", "status", "attemptId"],
+      "properties": {
+        "createdAt": {
+          "type": "string",
+          "format": "datetime",
+          "description": "The date and time of this write operation."
+        },
+        "attemptId": {
+          "type": "string",
+          "description": "The unique identifier for this instance of the age assurance flow, in UUID format."
+        },
+        "status": {
+          "type": "string",
+          "description": "The status of the Age Assurance process.",
+          "knownValues": ["unknown", "pending", "assured"]
+        },
+        "access": {
+          "type": "ref",
+          "ref": "app.bsky.ageassurance.defs#access"
+        },
+        "countryCode": {
+          "type": "string",
+          "description": "The ISO 3166-1 alpha-2 country code provided when beginning the Age Assurance flow."
+        },
+        "regionCode": {
+          "type": "string",
+          "description": "The ISO 3166-2 region code provided when beginning the Age Assurance flow."
+        },
+        "initIp": {
+          "type": "string",
+          "description": "The IP address used when initiating the AA flow."
+        },
+        "initUa": {
+          "type": "string",
+          "description": "The user agent used when initiating the AA flow."
+        },
+        "completeIp": {
+          "type": "string",
+          "description": "The IP address used when completing the AA flow."
+        },
+        "completeUa": {
+          "type": "string",
+          "description": "The user agent used when completing the AA flow."
+        }
+      }
+    },
+    "ageAssuranceOverrideEvent": {
+      "type": "object",
+      "description": "Age assurance status override by moderators. Only works on DID subjects.",
+      "required": ["comment", "status"],
+      "properties": {
+        "status": {
+          "type": "string",
+          "description": "The status to be set for the user decided by a moderator, overriding whatever value the user had previously. Use reset to default to original state.",
+          "knownValues": ["assured", "reset", "blocked"]
+        },
+        "access": {
+          "type": "ref",
+          "ref": "app.bsky.ageassurance.defs#access"
+        },
+        "comment": {
+          "type": "string",
+          "description": "Comment describing the reason for the override."
+        }
+      }
+    },
+    "revokeAccountCredentialsEvent": {
+      "type": "object",
+      "description": "Account credentials revocation by moderators. Only works on DID subjects.",
+      "required": ["comment"],
+      "properties": {
+        "comment": {
+          "type": "string",
+          "description": "Comment describing the reason for the revocation."
+        }
+      }
+    },
+    "modEventAcknowledge": {
+      "type": "object",
+      "properties": {
+        "comment": { "type": "string" },
+        "acknowledgeAccountSubjects": {
+          "type": "boolean",
+          "description": "If true, all other reports on content authored by this account will be resolved (acknowledged)."
+        }
+      }
+    },
+    "modEventEscalate": {
+      "type": "object",
+      "properties": {
+        "comment": { "type": "string" }
+      }
+    },
+    "modEventMute": {
+      "type": "object",
+      "description": "Mute incoming reports on a subject",
+      "required": ["durationInHours"],
+      "properties": {
+        "comment": { "type": "string" },
+        "durationInHours": {
+          "type": "integer",
+          "description": "Indicates how long the subject should remain muted."
+        }
+      }
+    },
+    "modEventUnmute": {
+      "type": "object",
+      "description": "Unmute action on a subject",
+      "properties": {
+        "comment": {
+          "type": "string",
+          "description": "Describe reasoning behind the reversal."
+        }
+      }
+    },
+    "modEventMuteReporter": {
+      "type": "object",
+      "description": "Mute incoming reports from an account",
+      "properties": {
+        "comment": { "type": "string" },
+        "durationInHours": {
+          "type": "integer",
+          "description": "Indicates how long the account should remain muted. Falsy value here means a permanent mute."
+        }
+      }
+    },
+    "modEventUnmuteReporter": {
+      "type": "object",
+      "description": "Unmute incoming reports from an account",
+      "properties": {
+        "comment": {
+          "type": "string",
+          "description": "Describe reasoning behind the reversal."
+        }
+      }
+    },
+    "modEventEmail": {
+      "type": "object",
+      "description": "Keep a log of outgoing email to a user",
+      "required": ["subjectLine"],
+      "properties": {
+        "subjectLine": {
+          "type": "string",
+          "description": "The subject line of the email sent to the user."
+        },
+        "content": {
+          "type": "string",
+          "description": "The content of the email sent to the user."
+        },
+        "comment": {
+          "type": "string",
+          "description": "Additional comment about the outgoing comm."
+        },
+        "policies": {
+          "type": "array",
+          "maxLength": 5,
+          "items": { "type": "string" },
+          "description": "Names/Keywords of the policies that necessitated the email."
+        },
+        "severityLevel": {
+          "type": "string",
+          "description": "Severity level of the violation. Normally 'sev-1' that adds strike on repeat offense"
+        },
+        "strikeCount": {
+          "type": "integer",
+          "description": "Number of strikes to assign to the user for this violation. Normally 0 as an indicator of a warning and only added as a strike on a repeat offense."
+        },
+        "strikeExpiresAt": {
+          "type": "string",
+          "format": "datetime",
+          "description": "When the strike should expire. If not provided, the strike never expires."
+        },
+        "isDelivered": {
+          "type": "boolean",
+          "description": "Indicates whether the email was successfully delivered to the user's inbox."
+        }
+      }
+    },
+    "modEventDivert": {
+      "type": "object",
+      "description": "Divert a record's blobs to a 3rd party service for further scanning/tagging",
+      "properties": {
+        "comment": { "type": "string" }
+      }
+    },
+    "modEventTag": {
+      "type": "object",
+      "description": "Add/Remove a tag on a subject",
+      "required": ["add", "remove"],
+      "properties": {
+        "add": {
+          "type": "array",
+          "items": { "type": "string" },
+          "description": "Tags to be added to the subject. If already exists, won't be duplicated."
+        },
+        "remove": {
+          "type": "array",
+          "items": { "type": "string" },
+          "description": "Tags to be removed to the subject. Ignores a tag If it doesn't exist, won't be duplicated."
+        },
+        "comment": {
+          "type": "string",
+          "description": "Additional comment about added/removed tags."
+        }
+      }
+    },
+    "accountEvent": {
+      "type": "object",
+      "description": "Logs account status related events on a repo subject. Normally captured by automod from the firehose and emitted to ozone for historical tracking.",
+      "required": ["timestamp", "active"],
+      "properties": {
+        "comment": { "type": "string" },
+        "active": {
+          "type": "boolean",
+          "description": "Indicates that the account has a repository which can be fetched from the host that emitted this event."
+        },
+        "status": {
+          "type": "string",
+          "knownValues": [
+            "unknown",
+            "deactivated",
+            "deleted",
+            "takendown",
+            "suspended",
+            "tombstoned"
+          ]
+        },
+        "timestamp": {
+          "type": "string",
+          "format": "datetime"
+        }
+      }
+    },
+    "identityEvent": {
+      "type": "object",
+      "description": "Logs identity related events on a repo subject. Normally captured by automod from the firehose and emitted to ozone for historical tracking.",
+      "required": ["timestamp"],
+      "properties": {
+        "comment": { "type": "string" },
+        "handle": { "type": "string", "format": "handle" },
+        "pdsHost": { "type": "string", "format": "uri" },
+        "tombstone": { "type": "boolean" },
+        "timestamp": {
+          "type": "string",
+          "format": "datetime"
+        }
+      }
+    },
+    "recordEvent": {
+      "type": "object",
+      "description": "Logs lifecycle event on a record subject. Normally captured by automod from the firehose and emitted to ozone for historical tracking.",
+      "required": ["timestamp", "op"],
+      "properties": {
+        "comment": { "type": "string" },
+        "op": {
+          "type": "string",
+          "knownValues": ["create", "update", "delete"]
+        },
+        "cid": { "type": "string", "format": "cid" },
+        "timestamp": { "type": "string", "format": "datetime" }
+      }
+    },
+    "scheduleTakedownEvent": {
+      "type": "object",
+      "description": "Logs a scheduled takedown action for an account.",
+      "properties": {
+        "comment": { "type": "string" },
+        "executeAt": { "type": "string", "format": "datetime" },
+        "executeAfter": { "type": "string", "format": "datetime" },
+        "executeUntil": { "type": "string", "format": "datetime" }
+      }
+    },
+    "cancelScheduledTakedownEvent": {
+      "type": "object",
+      "description": "Logs cancellation of a scheduled takedown action for an account.",
+      "properties": {
+        "comment": { "type": "string" }
+      }
+    },
+    "repoView": {
+      "type": "object",
+      "required": [
+        "did",
+        "handle",
+        "relatedRecords",
+        "indexedAt",
+        "moderation"
+      ],
+      "properties": {
+        "did": { "type": "string", "format": "did" },
+        "handle": { "type": "string", "format": "handle" },
+        "email": { "type": "string" },
+        "relatedRecords": { "type": "array", "items": { "type": "unknown" } },
+        "indexedAt": { "type": "string", "format": "datetime" },
+        "moderation": { "type": "ref", "ref": "#moderation" },
+        "invitedBy": {
+          "type": "ref",
+          "ref": "com.atproto.server.defs#inviteCode"
+        },
+        "invitesDisabled": { "type": "boolean" },
+        "inviteNote": { "type": "string" },
+        "deactivatedAt": { "type": "string", "format": "datetime" },
+        "threatSignatures": {
+          "type": "array",
+          "items": {
+            "type": "ref",
+            "ref": "com.atproto.admin.defs#threatSignature"
+          }
+        }
+      }
+    },
+    "repoViewDetail": {
+      "type": "object",
+      "required": [
+        "did",
+        "handle",
+        "relatedRecords",
+        "indexedAt",
+        "moderation"
+      ],
+      "properties": {
+        "did": { "type": "string", "format": "did" },
+        "handle": { "type": "string", "format": "handle" },
+        "email": { "type": "string" },
+        "relatedRecords": { "type": "array", "items": { "type": "unknown" } },
+        "indexedAt": { "type": "string", "format": "datetime" },
+        "moderation": { "type": "ref", "ref": "#moderationDetail" },
+        "labels": {
+          "type": "array",
+          "items": { "type": "ref", "ref": "com.atproto.label.defs#label" }
+        },
+        "invitedBy": {
+          "type": "ref",
+          "ref": "com.atproto.server.defs#inviteCode"
+        },
+        "invites": {
+          "type": "array",
+          "items": {
+            "type": "ref",
+            "ref": "com.atproto.server.defs#inviteCode"
+          }
+        },
+        "invitesDisabled": { "type": "boolean" },
+        "inviteNote": { "type": "string" },
+        "emailConfirmedAt": { "type": "string", "format": "datetime" },
+        "deactivatedAt": { "type": "string", "format": "datetime" },
+        "threatSignatures": {
+          "type": "array",
+          "items": {
+            "type": "ref",
+            "ref": "com.atproto.admin.defs#threatSignature"
+          }
+        }
+      }
+    },
+    "repoViewNotFound": {
+      "type": "object",
+      "required": ["did"],
+      "properties": {
+        "did": { "type": "string", "format": "did" }
+      }
+    },
+    "recordView": {
+      "type": "object",
+      "required": [
+        "uri",
+        "cid",
+        "value",
+        "blobCids",
+        "indexedAt",
+        "moderation",
+        "repo"
+      ],
+      "properties": {
+        "uri": { "type": "string", "format": "at-uri" },
+        "cid": { "type": "string", "format": "cid" },
+        "value": { "type": "unknown" },
+        "blobCids": {
+          "type": "array",
+          "items": { "type": "string", "format": "cid" }
+        },
+        "indexedAt": { "type": "string", "format": "datetime" },
+        "moderation": { "type": "ref", "ref": "#moderation" },
+        "repo": { "type": "ref", "ref": "#repoView" }
+      }
+    },
+    "recordViewDetail": {
+      "type": "object",
+      "required": [
+        "uri",
+        "cid",
+        "value",
+        "blobs",
+        "indexedAt",
+        "moderation",
+        "repo"
+      ],
+      "properties": {
+        "uri": { "type": "string", "format": "at-uri" },
+        "cid": { "type": "string", "format": "cid" },
+        "value": { "type": "unknown" },
+        "blobs": {
+          "type": "array",
+          "items": { "type": "ref", "ref": "#blobView" }
+        },
+        "labels": {
+          "type": "array",
+          "items": { "type": "ref", "ref": "com.atproto.label.defs#label" }
+        },
+        "indexedAt": { "type": "string", "format": "datetime" },
+        "moderation": { "type": "ref", "ref": "#moderationDetail" },
+        "repo": { "type": "ref", "ref": "#repoView" }
+      }
+    },
+    "recordViewNotFound": {
+      "type": "object",
+      "required": ["uri"],
+      "properties": {
+        "uri": { "type": "string", "format": "at-uri" }
+      }
+    },
+    "moderation": {
+      "type": "object",
+      "properties": {
+        "subjectStatus": { "type": "ref", "ref": "#subjectStatusView" }
+      }
+    },
+    "moderationDetail": {
+      "type": "object",
+      "properties": {
+        "subjectStatus": {
+          "type": "ref",
+          "ref": "#subjectStatusView"
+        }
+      }
+    },
+    "blobView": {
+      "type": "object",
+      "required": ["cid", "mimeType", "size", "createdAt"],
+      "properties": {
+        "cid": { "type": "string", "format": "cid" },
+        "mimeType": { "type": "string" },
+        "size": { "type": "integer" },
+        "createdAt": { "type": "string", "format": "datetime" },
+        "details": {
+          "type": "union",
+          "refs": ["#imageDetails", "#videoDetails"]
+        },
+        "moderation": { "type": "ref", "ref": "#moderation" }
+      }
+    },
+    "imageDetails": {
+      "type": "object",
+      "required": ["width", "height"],
+      "properties": {
+        "width": { "type": "integer" },
+        "height": { "type": "integer" }
+      }
+    },
+    "videoDetails": {
+      "type": "object",
+      "required": ["width", "height", "length"],
+      "properties": {
+        "width": { "type": "integer" },
+        "height": { "type": "integer" },
+        "length": { "type": "integer" }
+      }
+    },
+    "accountHosting": {
+      "type": "object",
+      "required": ["status"],
+      "properties": {
+        "status": {
+          "type": "string",
+          "knownValues": [
+            "takendown",
+            "suspended",
+            "deleted",
+            "deactivated",
+            "unknown"
+          ]
+        },
+        "updatedAt": {
+          "type": "string",
+          "format": "datetime"
+        },
+        "createdAt": {
+          "type": "string",
+          "format": "datetime"
+        },
+        "deletedAt": {
+          "type": "string",
+          "format": "datetime"
+        },
+        "deactivatedAt": {
+          "type": "string",
+          "format": "datetime"
+        },
+        "reactivatedAt": {
+          "type": "string",
+          "format": "datetime"
+        }
+      }
+    },
+    "recordHosting": {
+      "type": "object",
+      "required": ["status"],
+      "properties": {
+        "status": {
+          "type": "string",
+          "knownValues": ["deleted", "unknown"]
+        },
+        "updatedAt": {
+          "type": "string",
+          "format": "datetime"
+        },
+        "createdAt": {
+          "type": "string",
+          "format": "datetime"
+        },
+        "deletedAt": {
+          "type": "string",
+          "format": "datetime"
+        }
+      }
+    },
+    "reporterStats": {
+      "type": "object",
+      "required": [
+        "did",
+        "accountReportCount",
+        "recordReportCount",
+        "reportedAccountCount",
+        "reportedRecordCount",
+        "takendownAccountCount",
+        "takendownRecordCount",
+        "labeledAccountCount",
+        "labeledRecordCount"
+      ],
+      "properties": {
+        "did": {
+          "type": "string",
+          "format": "did"
+        },
+        "accountReportCount": {
+          "type": "integer",
+          "description": "The total number of reports made by the user on accounts."
+        },
+        "recordReportCount": {
+          "type": "integer",
+          "description": "The total number of reports made by the user on records."
+        },
+        "reportedAccountCount": {
+          "type": "integer",
+          "description": "The total number of accounts reported by the user."
+        },
+        "reportedRecordCount": {
+          "type": "integer",
+          "description": "The total number of records reported by the user."
+        },
+        "takendownAccountCount": {
+          "type": "integer",
+          "description": "The total number of accounts taken down as a result of the user's reports."
+        },
+        "takendownRecordCount": {
+          "type": "integer",
+          "description": "The total number of records taken down as a result of the user's reports."
+        },
+        "labeledAccountCount": {
+          "type": "integer",
+          "description": "The total number of accounts labeled as a result of the user's reports."
+        },
+        "labeledRecordCount": {
+          "type": "integer",
+          "description": "The total number of records labeled as a result of the user's reports."
+        }
+      }
+    },
+    "modTool": {
+      "type": "object",
+      "description": "Moderation tool information for tracing the source of the action",
+      "required": ["name"],
+      "properties": {
+        "name": {
+          "type": "string",
+          "description": "Name/identifier of the source (e.g., 'automod', 'ozone/workspace')"
+        },
+        "meta": {
+          "type": "unknown",
+          "description": "Additional arbitrary metadata about the source"
+        }
+      }
+    },
+    "timelineEventPlcCreate": {
+      "type": "token",
+      "description": "Moderation event timeline event for a PLC create operation"
+    },
+    "timelineEventPlcOperation": {
+      "type": "token",
+      "description": "Moderation event timeline event for generic PLC operation"
+    },
+    "timelineEventPlcTombstone": {
+      "type": "token",
+      "description": "Moderation event timeline event for a PLC tombstone operation"
+    },
+    "scheduledActionView": {
+      "type": "object",
+      "description": "View of a scheduled moderation action",
+      "required": ["id", "action", "did", "createdBy", "createdAt", "status"],
+      "properties": {
+        "id": {
+          "type": "integer",
+          "description": "Auto-incrementing row ID"
+        },
+        "action": {
+          "type": "string",
+          "knownValues": ["takedown"],
+          "description": "Type of action to be executed"
+        },
+        "eventData": {
+          "type": "unknown",
+          "description": "Serialized event object that will be propagated to the event when performed"
+        },
+        "did": {
+          "type": "string",
+          "format": "did",
+          "description": "Subject DID for the action"
+        },
+        "executeAt": {
+          "type": "string",
+          "format": "datetime",
+          "description": "Exact time to execute the action"
+        },
+        "executeAfter": {
+          "type": "string",
+          "format": "datetime",
+          "description": "Earliest time to execute the action (for randomized scheduling)"
+        },
+        "executeUntil": {
+          "type": "string",
+          "format": "datetime",
+          "description": "Latest time to execute the action (for randomized scheduling)"
+        },
+        "randomizeExecution": {
+          "type": "boolean",
+          "description": "Whether execution time should be randomized within the specified range"
+        },
+        "createdBy": {
+          "type": "string",
+          "format": "did",
+          "description": "DID of the user who created this scheduled action"
+        },
+        "createdAt": {
+          "type": "string",
+          "format": "datetime",
+          "description": "When the scheduled action was created"
+        },
+        "updatedAt": {
+          "type": "string",
+          "format": "datetime",
+          "description": "When the scheduled action was last updated"
+        },
+        "status": {
+          "type": "string",
+          "knownValues": ["pending", "executed", "cancelled", "failed"],
+          "description": "Current status of the scheduled action"
+        },
+        "lastExecutedAt": {
+          "type": "string",
+          "format": "datetime",
+          "description": "When the action was last attempted to be executed"
+        },
+        "lastFailureReason": {
+          "type": "string",
+          "description": "Reason for the last execution failure"
+        },
+        "executionEventId": {
+          "type": "integer",
+          "description": "ID of the moderation event created when action was successfully executed"
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/tools/ozone/moderation/emitEvent.json b/lexicons/tools/ozone/moderation/emitEvent.json
new file mode 100644
index 00000000..ddce7921
--- /dev/null
+++ b/lexicons/tools/ozone/moderation/emitEvent.json
@@ -0,0 +1,90 @@
+{
+  "lexicon": 1,
+  "id": "tools.ozone.moderation.emitEvent",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Take a moderation action on an actor.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["event", "subject", "createdBy"],
+          "properties": {
+            "event": {
+              "type": "union",
+              "refs": [
+                "tools.ozone.moderation.defs#modEventTakedown",
+                "tools.ozone.moderation.defs#modEventAcknowledge",
+                "tools.ozone.moderation.defs#modEventEscalate",
+                "tools.ozone.moderation.defs#modEventComment",
+                "tools.ozone.moderation.defs#modEventLabel",
+                "tools.ozone.moderation.defs#modEventReport",
+                "tools.ozone.moderation.defs#modEventMute",
+                "tools.ozone.moderation.defs#modEventUnmute",
+                "tools.ozone.moderation.defs#modEventMuteReporter",
+                "tools.ozone.moderation.defs#modEventUnmuteReporter",
+                "tools.ozone.moderation.defs#modEventReverseTakedown",
+                "tools.ozone.moderation.defs#modEventResolveAppeal",
+                "tools.ozone.moderation.defs#modEventEmail",
+                "tools.ozone.moderation.defs#modEventDivert",
+                "tools.ozone.moderation.defs#modEventTag",
+                "tools.ozone.moderation.defs#accountEvent",
+                "tools.ozone.moderation.defs#identityEvent",
+                "tools.ozone.moderation.defs#recordEvent",
+                "tools.ozone.moderation.defs#modEventPriorityScore",
+                "tools.ozone.moderation.defs#ageAssuranceEvent",
+                "tools.ozone.moderation.defs#ageAssuranceOverrideEvent",
+                "tools.ozone.moderation.defs#revokeAccountCredentialsEvent",
+                "tools.ozone.moderation.defs#scheduleTakedownEvent",
+                "tools.ozone.moderation.defs#cancelScheduledTakedownEvent"
+              ]
+            },
+            "subject": {
+              "type": "union",
+              "refs": [
+                "com.atproto.admin.defs#repoRef",
+                "com.atproto.repo.strongRef"
+              ]
+            },
+            "subjectBlobCids": {
+              "type": "array",
+              "items": {
+                "type": "string",
+                "format": "cid"
+              }
+            },
+            "createdBy": {
+              "type": "string",
+              "format": "did"
+            },
+            "modTool": {
+              "type": "ref",
+              "ref": "tools.ozone.moderation.defs#modTool"
+            },
+            "externalId": {
+              "type": "string",
+              "description": "An optional external ID for the event, used to deduplicate events from external systems. Fails when an event of same type with the same external ID exists for the same subject."
+            }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "ref",
+          "ref": "tools.ozone.moderation.defs#modEventView"
+        }
+      },
+      "errors": [
+        {
+          "name": "SubjectHasAction"
+        },
+        {
+          "name": "DuplicateExternalId",
+          "description": "An event with the same external ID already exists for the subject."
+        }
+      ]
+    }
+  }
+}
diff --git a/lexicons/tools/ozone/moderation/getAccountTimeline.json b/lexicons/tools/ozone/moderation/getAccountTimeline.json
new file mode 100644
index 00000000..6dcdd595
--- /dev/null
+++ b/lexicons/tools/ozone/moderation/getAccountTimeline.json
@@ -0,0 +1,106 @@
+{
+  "lexicon": 1,
+  "id": "tools.ozone.moderation.getAccountTimeline",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get timeline of all available events of an account. This includes moderation events, account history and did history.",
+      "parameters": {
+        "type": "params",
+        "required": ["did"],
+        "properties": {
+          "did": {
+            "type": "string",
+            "format": "did"
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["timeline"],
+          "properties": {
+            "timeline": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "#timelineItem"
+              }
+            }
+          }
+        }
+      },
+      "errors": [
+        {
+          "name": "RepoNotFound"
+        }
+      ]
+    },
+    "timelineItem": {
+      "type": "object",
+      "required": ["day", "summary"],
+      "properties": {
+        "day": {
+          "type": "string"
+        },
+        "summary": {
+          "type": "array",
+          "items": {
+            "type": "ref",
+            "ref": "#timelineItemSummary"
+          }
+        }
+      }
+    },
+    "timelineItemSummary": {
+      "type": "object",
+      "required": ["eventSubjectType", "eventType", "count"],
+      "properties": {
+        "eventSubjectType": {
+          "type": "string",
+          "knownValues": ["account", "record", "chat"]
+        },
+        "eventType": {
+          "type": "string",
+          "knownValues": [
+            "tools.ozone.moderation.defs#modEventTakedown",
+            "tools.ozone.moderation.defs#modEventReverseTakedown",
+            "tools.ozone.moderation.defs#modEventComment",
+            "tools.ozone.moderation.defs#modEventReport",
+            "tools.ozone.moderation.defs#modEventLabel",
+            "tools.ozone.moderation.defs#modEventAcknowledge",
+            "tools.ozone.moderation.defs#modEventEscalate",
+            "tools.ozone.moderation.defs#modEventMute",
+            "tools.ozone.moderation.defs#modEventUnmute",
+            "tools.ozone.moderation.defs#modEventMuteReporter",
+            "tools.ozone.moderation.defs#modEventUnmuteReporter",
+            "tools.ozone.moderation.defs#modEventEmail",
+            "tools.ozone.moderation.defs#modEventResolveAppeal",
+            "tools.ozone.moderation.defs#modEventDivert",
+            "tools.ozone.moderation.defs#modEventTag",
+            "tools.ozone.moderation.defs#accountEvent",
+            "tools.ozone.moderation.defs#identityEvent",
+            "tools.ozone.moderation.defs#recordEvent",
+            "tools.ozone.moderation.defs#modEventPriorityScore",
+            "tools.ozone.moderation.defs#revokeAccountCredentialsEvent",
+            "tools.ozone.moderation.defs#ageAssuranceEvent",
+            "tools.ozone.moderation.defs#ageAssuranceOverrideEvent",
+            "tools.ozone.moderation.defs#timelineEventPlcCreate",
+            "tools.ozone.moderation.defs#timelineEventPlcOperation",
+            "tools.ozone.moderation.defs#timelineEventPlcTombstone",
+            "tools.ozone.hosting.getAccountHistory#accountCreated",
+            "tools.ozone.hosting.getAccountHistory#emailConfirmed",
+            "tools.ozone.hosting.getAccountHistory#passwordUpdated",
+            "tools.ozone.hosting.getAccountHistory#handleUpdated",
+            "tools.ozone.moderation.defs#scheduleTakedownEvent",
+            "tools.ozone.moderation.defs#cancelScheduledTakedownEvent"
+          ]
+        },
+        "count": {
+          "type": "integer"
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/tools/ozone/moderation/getEvent.json b/lexicons/tools/ozone/moderation/getEvent.json
new file mode 100644
index 00000000..66d7642e
--- /dev/null
+++ b/lexicons/tools/ozone/moderation/getEvent.json
@@ -0,0 +1,24 @@
+{
+  "lexicon": 1,
+  "id": "tools.ozone.moderation.getEvent",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get details about a moderation event.",
+      "parameters": {
+        "type": "params",
+        "required": ["id"],
+        "properties": {
+          "id": { "type": "integer" }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "ref",
+          "ref": "tools.ozone.moderation.defs#modEventViewDetail"
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/tools/ozone/moderation/getRecord.json b/lexicons/tools/ozone/moderation/getRecord.json
new file mode 100644
index 00000000..ca5c4806
--- /dev/null
+++ b/lexicons/tools/ozone/moderation/getRecord.json
@@ -0,0 +1,26 @@
+{
+  "lexicon": 1,
+  "id": "tools.ozone.moderation.getRecord",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get details about a record.",
+      "parameters": {
+        "type": "params",
+        "required": ["uri"],
+        "properties": {
+          "uri": { "type": "string", "format": "at-uri" },
+          "cid": { "type": "string", "format": "cid" }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "ref",
+          "ref": "tools.ozone.moderation.defs#recordViewDetail"
+        }
+      },
+      "errors": [{ "name": "RecordNotFound" }]
+    }
+  }
+}
diff --git a/lexicons/tools/ozone/moderation/getRecords.json b/lexicons/tools/ozone/moderation/getRecords.json
new file mode 100644
index 00000000..12dc1f68
--- /dev/null
+++ b/lexicons/tools/ozone/moderation/getRecords.json
@@ -0,0 +1,43 @@
+{
+  "lexicon": 1,
+  "id": "tools.ozone.moderation.getRecords",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get details about some records.",
+      "parameters": {
+        "type": "params",
+        "required": ["uris"],
+        "properties": {
+          "uris": {
+            "type": "array",
+            "maxLength": 100,
+            "items": {
+              "type": "string",
+              "format": "at-uri"
+            }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["records"],
+          "properties": {
+            "records": {
+              "type": "array",
+              "items": {
+                "type": "union",
+                "refs": [
+                  "tools.ozone.moderation.defs#recordViewDetail",
+                  "tools.ozone.moderation.defs#recordViewNotFound"
+                ]
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/tools/ozone/moderation/getRepo.json b/lexicons/tools/ozone/moderation/getRepo.json
new file mode 100644
index 00000000..b5af1f07
--- /dev/null
+++ b/lexicons/tools/ozone/moderation/getRepo.json
@@ -0,0 +1,25 @@
+{
+  "lexicon": 1,
+  "id": "tools.ozone.moderation.getRepo",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get details about a repository.",
+      "parameters": {
+        "type": "params",
+        "required": ["did"],
+        "properties": {
+          "did": { "type": "string", "format": "did" }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "ref",
+          "ref": "tools.ozone.moderation.defs#repoViewDetail"
+        }
+      },
+      "errors": [{ "name": "RepoNotFound" }]
+    }
+  }
+}
diff --git a/lexicons/tools/ozone/moderation/getReporterStats.json b/lexicons/tools/ozone/moderation/getReporterStats.json
new file mode 100644
index 00000000..51f44541
--- /dev/null
+++ b/lexicons/tools/ozone/moderation/getReporterStats.json
@@ -0,0 +1,40 @@
+{
+  "lexicon": 1,
+  "id": "tools.ozone.moderation.getReporterStats",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get reporter stats for a list of users.",
+      "parameters": {
+        "type": "params",
+        "required": ["dids"],
+        "properties": {
+          "dids": {
+            "type": "array",
+            "maxLength": 100,
+            "items": {
+              "type": "string",
+              "format": "did"
+            }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["stats"],
+          "properties": {
+            "stats": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "tools.ozone.moderation.defs#reporterStats"
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/tools/ozone/moderation/getRepos.json b/lexicons/tools/ozone/moderation/getRepos.json
new file mode 100644
index 00000000..3124db56
--- /dev/null
+++ b/lexicons/tools/ozone/moderation/getRepos.json
@@ -0,0 +1,43 @@
+{
+  "lexicon": 1,
+  "id": "tools.ozone.moderation.getRepos",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get details about some repositories.",
+      "parameters": {
+        "type": "params",
+        "required": ["dids"],
+        "properties": {
+          "dids": {
+            "type": "array",
+            "maxLength": 100,
+            "items": {
+              "type": "string",
+              "format": "did"
+            }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["repos"],
+          "properties": {
+            "repos": {
+              "type": "array",
+              "items": {
+                "type": "union",
+                "refs": [
+                  "tools.ozone.moderation.defs#repoViewDetail",
+                  "tools.ozone.moderation.defs#repoViewNotFound"
+                ]
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/tools/ozone/moderation/getSubjects.json b/lexicons/tools/ozone/moderation/getSubjects.json
new file mode 100644
index 00000000..5bedeb39
--- /dev/null
+++ b/lexicons/tools/ozone/moderation/getSubjects.json
@@ -0,0 +1,40 @@
+{
+  "lexicon": 1,
+  "id": "tools.ozone.moderation.getSubjects",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get details about subjects.",
+      "parameters": {
+        "type": "params",
+        "required": ["subjects"],
+        "properties": {
+          "subjects": {
+            "type": "array",
+            "maxLength": 100,
+            "minLength": 1,
+            "items": {
+              "type": "string"
+            }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["subjects"],
+          "properties": {
+            "subjects": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "tools.ozone.moderation.defs#subjectView"
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/tools/ozone/moderation/listScheduledActions.json b/lexicons/tools/ozone/moderation/listScheduledActions.json
new file mode 100644
index 00000000..feb06b43
--- /dev/null
+++ b/lexicons/tools/ozone/moderation/listScheduledActions.json
@@ -0,0 +1,78 @@
+{
+  "lexicon": 1,
+  "id": "tools.ozone.moderation.listScheduledActions",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "List scheduled moderation actions with optional filtering",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["statuses"],
+          "properties": {
+            "startsAfter": {
+              "type": "string",
+              "format": "datetime",
+              "description": "Filter actions scheduled to execute after this time"
+            },
+            "endsBefore": {
+              "type": "string",
+              "format": "datetime",
+              "description": "Filter actions scheduled to execute before this time"
+            },
+            "subjects": {
+              "type": "array",
+              "maxLength": 100,
+              "items": {
+                "type": "string",
+                "format": "did"
+              },
+              "description": "Filter actions for specific DID subjects"
+            },
+            "statuses": {
+              "type": "array",
+              "minLength": 1,
+              "items": {
+                "type": "string",
+                "knownValues": ["pending", "executed", "cancelled", "failed"]
+              },
+              "description": "Filter actions by status"
+            },
+            "limit": {
+              "type": "integer",
+              "minimum": 1,
+              "maximum": 100,
+              "default": 50,
+              "description": "Maximum number of results to return"
+            },
+            "cursor": {
+              "type": "string",
+              "description": "Cursor for pagination"
+            }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["actions"],
+          "properties": {
+            "actions": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "tools.ozone.moderation.defs#scheduledActionView"
+              }
+            },
+            "cursor": {
+              "type": "string",
+              "description": "Cursor for next page of results"
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/tools/ozone/moderation/queryEvents.json b/lexicons/tools/ozone/moderation/queryEvents.json
new file mode 100644
index 00000000..4c011d6c
--- /dev/null
+++ b/lexicons/tools/ozone/moderation/queryEvents.json
@@ -0,0 +1,162 @@
+{
+  "lexicon": 1,
+  "id": "tools.ozone.moderation.queryEvents",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "List moderation events related to a subject.",
+      "parameters": {
+        "type": "params",
+        "properties": {
+          "types": {
+            "type": "array",
+            "items": {
+              "type": "string"
+            },
+            "description": "The types of events (fully qualified string in the format of tools.ozone.moderation.defs#modEvent<name>) to filter by. If not specified, all events are returned."
+          },
+          "createdBy": {
+            "type": "string",
+            "format": "did"
+          },
+          "sortDirection": {
+            "type": "string",
+            "default": "desc",
+            "enum": ["asc", "desc"],
+            "description": "Sort direction for the events. Defaults to descending order of created at timestamp."
+          },
+          "createdAfter": {
+            "type": "string",
+            "format": "datetime",
+            "description": "Retrieve events created after a given timestamp"
+          },
+          "createdBefore": {
+            "type": "string",
+            "format": "datetime",
+            "description": "Retrieve events created before a given timestamp"
+          },
+          "subject": {
+            "type": "string",
+            "format": "uri"
+          },
+          "collections": {
+            "type": "array",
+            "maxLength": 20,
+            "description": "If specified, only events where the subject belongs to the given collections will be returned. When subjectType is set to 'account', this will be ignored.",
+            "items": {
+              "type": "string",
+              "format": "nsid"
+            }
+          },
+          "subjectType": {
+            "type": "string",
+            "description": "If specified, only events where the subject is of the given type (account or record) will be returned. When this is set to 'account' the 'collections' parameter will be ignored. When includeAllUserRecords or subject is set, this will be ignored.",
+            "knownValues": ["account", "record"]
+          },
+          "includeAllUserRecords": {
+            "type": "boolean",
+            "default": false,
+            "description": "If true, events on all record types (posts, lists, profile etc.) or records from given 'collections' param, owned by the did are returned."
+          },
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 100,
+            "default": 50
+          },
+          "hasComment": {
+            "type": "boolean",
+            "description": "If true, only events with comments are returned"
+          },
+          "comment": {
+            "type": "string",
+            "description": "If specified, only events with comments containing the keyword are returned. Apply || separator to use multiple keywords and match using OR condition."
+          },
+          "addedLabels": {
+            "type": "array",
+            "items": {
+              "type": "string"
+            },
+            "description": "If specified, only events where all of these labels were added are returned"
+          },
+          "removedLabels": {
+            "type": "array",
+            "items": {
+              "type": "string"
+            },
+            "description": "If specified, only events where all of these labels were removed are returned"
+          },
+          "addedTags": {
+            "type": "array",
+            "items": {
+              "type": "string"
+            },
+            "description": "If specified, only events where all of these tags were added are returned"
+          },
+          "removedTags": {
+            "type": "array",
+            "items": {
+              "type": "string"
+            },
+            "description": "If specified, only events where all of these tags were removed are returned"
+          },
+          "reportTypes": {
+            "type": "array",
+            "items": {
+              "type": "string"
+            }
+          },
+          "policies": {
+            "type": "array",
+            "items": {
+              "type": "string",
+              "description": "If specified, only events where the action policies match any of the given policies are returned"
+            }
+          },
+          "modTool": {
+            "type": "array",
+            "items": {
+              "type": "string"
+            },
+            "description": "If specified, only events where the modTool name matches any of the given values are returned"
+          },
+          "batchId": {
+            "type": "string",
+            "description": "If specified, only events where the batchId matches the given value are returned"
+          },
+          "ageAssuranceState": {
+            "type": "string",
+            "description": "If specified, only events where the age assurance state matches the given value are returned",
+            "knownValues": ["pending", "assured", "unknown", "reset", "blocked"]
+          },
+          "withStrike": {
+            "type": "boolean",
+            "description": "If specified, only events where strikeCount value is set are returned."
+          },
+          "cursor": {
+            "type": "string"
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["events"],
+          "properties": {
+            "cursor": {
+              "type": "string"
+            },
+            "events": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "tools.ozone.moderation.defs#modEventView"
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/tools/ozone/moderation/queryStatuses.json b/lexicons/tools/ozone/moderation/queryStatuses.json
new file mode 100644
index 00000000..738d83ee
--- /dev/null
+++ b/lexicons/tools/ozone/moderation/queryStatuses.json
@@ -0,0 +1,225 @@
+{
+  "lexicon": 1,
+  "id": "tools.ozone.moderation.queryStatuses",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "View moderation statuses of subjects (record or repo).",
+      "parameters": {
+        "type": "params",
+        "properties": {
+          "queueCount": {
+            "type": "integer",
+            "description": "Number of queues being used by moderators. Subjects will be split among all queues."
+          },
+          "queueIndex": {
+            "type": "integer",
+            "description": "Index of the queue to fetch subjects from. Works only when queueCount value is specified."
+          },
+          "queueSeed": {
+            "type": "string",
+            "description": "A seeder to shuffle/balance the queue items."
+          },
+          "includeAllUserRecords": {
+            "type": "boolean",
+            "description": "All subjects, or subjects from given 'collections' param, belonging to the account specified in the 'subject' param will be returned."
+          },
+          "subject": {
+            "type": "string",
+            "format": "uri",
+            "description": "The subject to get the status for."
+          },
+          "comment": {
+            "type": "string",
+            "description": "Search subjects by keyword from comments"
+          },
+          "reportedAfter": {
+            "type": "string",
+            "format": "datetime",
+            "description": "Search subjects reported after a given timestamp"
+          },
+          "reportedBefore": {
+            "type": "string",
+            "format": "datetime",
+            "description": "Search subjects reported before a given timestamp"
+          },
+          "reviewedAfter": {
+            "type": "string",
+            "format": "datetime",
+            "description": "Search subjects reviewed after a given timestamp"
+          },
+          "hostingDeletedAfter": {
+            "type": "string",
+            "format": "datetime",
+            "description": "Search subjects where the associated record/account was deleted after a given timestamp"
+          },
+          "hostingDeletedBefore": {
+            "type": "string",
+            "format": "datetime",
+            "description": "Search subjects where the associated record/account was deleted before a given timestamp"
+          },
+          "hostingUpdatedAfter": {
+            "type": "string",
+            "format": "datetime",
+            "description": "Search subjects where the associated record/account was updated after a given timestamp"
+          },
+          "hostingUpdatedBefore": {
+            "type": "string",
+            "format": "datetime",
+            "description": "Search subjects where the associated record/account was updated before a given timestamp"
+          },
+          "hostingStatuses": {
+            "type": "array",
+            "items": {
+              "type": "string"
+            },
+            "description": "Search subjects by the status of the associated record/account"
+          },
+          "reviewedBefore": {
+            "type": "string",
+            "format": "datetime",
+            "description": "Search subjects reviewed before a given timestamp"
+          },
+          "includeMuted": {
+            "type": "boolean",
+            "description": "By default, we don't include muted subjects in the results. Set this to true to include them."
+          },
+          "onlyMuted": {
+            "type": "boolean",
+            "description": "When set to true, only muted subjects and reporters will be returned."
+          },
+          "reviewState": {
+            "type": "string",
+            "description": "Specify when fetching subjects in a certain state",
+            "knownValues": [
+              "tools.ozone.moderation.defs#reviewOpen",
+              "tools.ozone.moderation.defs#reviewClosed",
+              "tools.ozone.moderation.defs#reviewEscalated",
+              "tools.ozone.moderation.defs#reviewNone"
+            ]
+          },
+          "ignoreSubjects": {
+            "type": "array",
+            "items": {
+              "type": "string",
+              "format": "uri"
+            }
+          },
+          "lastReviewedBy": {
+            "type": "string",
+            "format": "did",
+            "description": "Get all subject statuses that were reviewed by a specific moderator"
+          },
+          "sortField": {
+            "type": "string",
+            "default": "lastReportedAt",
+            "enum": [
+              "lastReviewedAt",
+              "lastReportedAt",
+              "reportedRecordsCount",
+              "takendownRecordsCount",
+              "priorityScore"
+            ]
+          },
+          "sortDirection": {
+            "type": "string",
+            "default": "desc",
+            "enum": ["asc", "desc"]
+          },
+          "takendown": {
+            "type": "boolean",
+            "description": "Get subjects that were taken down"
+          },
+          "appealed": {
+            "type": "boolean",
+            "description": "Get subjects in unresolved appealed status"
+          },
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 100,
+            "default": 50
+          },
+          "tags": {
+            "type": "array",
+            "maxLength": 25,
+            "items": {
+              "type": "string",
+              "description": "Items in this array are applied with OR filters. To apply AND filter, put all tags in the same string and separate using && characters"
+            }
+          },
+          "excludeTags": {
+            "type": "array",
+            "items": {
+              "type": "string"
+            }
+          },
+          "cursor": {
+            "type": "string"
+          },
+          "collections": {
+            "type": "array",
+            "maxLength": 20,
+            "description": "If specified, subjects belonging to the given collections will be returned. When subjectType is set to 'account', this will be ignored.",
+            "items": {
+              "type": "string",
+              "format": "nsid"
+            }
+          },
+          "subjectType": {
+            "type": "string",
+            "description": "If specified, subjects of the given type (account or record) will be returned. When this is set to 'account' the 'collections' parameter will be ignored. When includeAllUserRecords or subject is set, this will be ignored.",
+            "knownValues": ["account", "record"]
+          },
+          "minAccountSuspendCount": {
+            "type": "integer",
+            "description": "If specified, only subjects that belong to an account that has at least this many suspensions will be returned."
+          },
+          "minReportedRecordsCount": {
+            "type": "integer",
+            "description": "If specified, only subjects that belong to an account that has at least this many reported records will be returned."
+          },
+          "minTakendownRecordsCount": {
+            "type": "integer",
+            "description": "If specified, only subjects that belong to an account that has at least this many taken down records will be returned."
+          },
+          "minPriorityScore": {
+            "minimum": 0,
+            "maximum": 100,
+            "type": "integer",
+            "description": "If specified, only subjects that have priority score value above the given value will be returned."
+          },
+          "minStrikeCount": {
+            "type": "integer",
+            "minimum": 1,
+            "description": "If specified, only subjects that belong to an account that has at least this many active strikes will be returned."
+          },
+          "ageAssuranceState": {
+            "type": "string",
+            "description": "If specified, only subjects with the given age assurance state will be returned.",
+            "knownValues": ["pending", "assured", "unknown", "reset", "blocked"]
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["subjectStatuses"],
+          "properties": {
+            "cursor": {
+              "type": "string"
+            },
+            "subjectStatuses": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "tools.ozone.moderation.defs#subjectStatusView"
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/tools/ozone/moderation/scheduleAction.json b/lexicons/tools/ozone/moderation/scheduleAction.json
new file mode 100644
index 00000000..d13959ea
--- /dev/null
+++ b/lexicons/tools/ozone/moderation/scheduleAction.json
@@ -0,0 +1,155 @@
+{
+  "lexicon": 1,
+  "id": "tools.ozone.moderation.scheduleAction",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Schedule a moderation action to be executed at a future time",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["action", "subjects", "createdBy", "scheduling"],
+          "properties": {
+            "action": {
+              "type": "union",
+              "refs": ["#takedown"]
+            },
+            "subjects": {
+              "type": "array",
+              "maxLength": 100,
+              "items": {
+                "type": "string",
+                "format": "did"
+              },
+              "description": "Array of DID subjects to schedule the action for"
+            },
+            "createdBy": {
+              "type": "string",
+              "format": "did"
+            },
+            "scheduling": {
+              "type": "ref",
+              "ref": "#schedulingConfig"
+            },
+            "modTool": {
+              "type": "ref",
+              "ref": "tools.ozone.moderation.defs#modTool",
+              "description": "This will be propagated to the moderation event when it is applied"
+            }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "ref",
+          "ref": "#scheduledActionResults"
+        }
+      }
+    },
+    "takedown": {
+      "type": "object",
+      "description": "Schedule a takedown action",
+      "properties": {
+        "comment": {
+          "type": "string"
+        },
+        "durationInHours": {
+          "type": "integer",
+          "description": "Indicates how long the takedown should be in effect before automatically expiring."
+        },
+        "acknowledgeAccountSubjects": {
+          "type": "boolean",
+          "description": "If true, all other reports on content authored by this account will be resolved (acknowledged)."
+        },
+        "policies": {
+          "type": "array",
+          "maxLength": 5,
+          "items": {
+            "type": "string"
+          },
+          "description": "Names/Keywords of the policies that drove the decision."
+        },
+        "severityLevel": {
+          "type": "string",
+          "description": "Severity level of the violation (e.g., 'sev-0', 'sev-1', 'sev-2', etc.)."
+        },
+        "strikeCount": {
+          "type": "integer",
+          "description": "Number of strikes to assign to the user when takedown is applied."
+        },
+        "strikeExpiresAt": {
+          "type": "string",
+          "format": "datetime",
+          "description": "When the strike should expire. If not provided, the strike never expires."
+        },
+        "emailContent": {
+          "type": "string",
+          "description": "Email content to be sent to the user upon takedown."
+        },
+        "emailSubject": {
+          "type": "string",
+          "description": "Subject of the email to be sent to the user upon takedown."
+        }
+      }
+    },
+    "schedulingConfig": {
+      "type": "object",
+      "description": "Configuration for when the action should be executed",
+      "properties": {
+        "executeAt": {
+          "type": "string",
+          "format": "datetime",
+          "description": "Exact time to execute the action"
+        },
+        "executeAfter": {
+          "type": "string",
+          "format": "datetime",
+          "description": "Earliest time to execute the action (for randomized scheduling)"
+        },
+        "executeUntil": {
+          "type": "string",
+          "format": "datetime",
+          "description": "Latest time to execute the action (for randomized scheduling)"
+        }
+      }
+    },
+    "scheduledActionResults": {
+      "type": "object",
+      "required": ["succeeded", "failed"],
+      "properties": {
+        "succeeded": {
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "did"
+          }
+        },
+        "failed": {
+          "type": "array",
+          "items": {
+            "type": "ref",
+            "ref": "#failedScheduling"
+          }
+        }
+      }
+    },
+    "failedScheduling": {
+      "type": "object",
+      "required": ["subject", "error"],
+      "properties": {
+        "subject": {
+          "type": "string",
+          "format": "did"
+        },
+        "error": {
+          "type": "string"
+        },
+        "errorCode": {
+          "type": "string"
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/tools/ozone/moderation/searchRepos.json b/lexicons/tools/ozone/moderation/searchRepos.json
new file mode 100644
index 00000000..3968b340
--- /dev/null
+++ b/lexicons/tools/ozone/moderation/searchRepos.json
@@ -0,0 +1,44 @@
+{
+  "lexicon": 1,
+  "id": "tools.ozone.moderation.searchRepos",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Find repositories based on a search term.",
+      "parameters": {
+        "type": "params",
+        "properties": {
+          "term": {
+            "type": "string",
+            "description": "DEPRECATED: use 'q' instead"
+          },
+          "q": { "type": "string" },
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 100,
+            "default": 50
+          },
+          "cursor": { "type": "string" }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["repos"],
+          "properties": {
+            "cursor": { "type": "string" },
+            "repos": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "tools.ozone.moderation.defs#repoView"
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/tools/ozone/report/defs.json b/lexicons/tools/ozone/report/defs.json
new file mode 100644
index 00000000..5eca2699
--- /dev/null
+++ b/lexicons/tools/ozone/report/defs.json
@@ -0,0 +1,223 @@
+{
+  "lexicon": 1,
+  "id": "tools.ozone.report.defs",
+  "defs": {
+    "reasonType": {
+      "type": "string",
+      "knownValues": [
+        "tools.ozone.report.defs#reasonAppeal",
+        "tools.ozone.report.defs#reasonOther",
+
+        "tools.ozone.report.defs#reasonViolenceAnimal",
+        "tools.ozone.report.defs#reasonViolenceThreats",
+        "tools.ozone.report.defs#reasonViolenceGraphicContent",
+        "tools.ozone.report.defs#reasonViolenceGlorification",
+        "tools.ozone.report.defs#reasonViolenceExtremistContent",
+        "tools.ozone.report.defs#reasonViolenceTrafficking",
+        "tools.ozone.report.defs#reasonViolenceOther",
+
+        "tools.ozone.report.defs#reasonSexualAbuseContent",
+        "tools.ozone.report.defs#reasonSexualNCII",
+        "tools.ozone.report.defs#reasonSexualDeepfake",
+        "tools.ozone.report.defs#reasonSexualAnimal",
+        "tools.ozone.report.defs#reasonSexualUnlabeled",
+        "tools.ozone.report.defs#reasonSexualOther",
+
+        "tools.ozone.report.defs#reasonChildSafetyCSAM",
+        "tools.ozone.report.defs#reasonChildSafetyGroom",
+        "tools.ozone.report.defs#reasonChildSafetyPrivacy",
+        "tools.ozone.report.defs#reasonChildSafetyHarassment",
+        "tools.ozone.report.defs#reasonChildSafetyOther",
+
+        "tools.ozone.report.defs#reasonHarassmentTroll",
+        "tools.ozone.report.defs#reasonHarassmentTargeted",
+        "tools.ozone.report.defs#reasonHarassmentHateSpeech",
+        "tools.ozone.report.defs#reasonHarassmentDoxxing",
+        "tools.ozone.report.defs#reasonHarassmentOther",
+
+        "tools.ozone.report.defs#reasonMisleadingBot",
+        "tools.ozone.report.defs#reasonMisleadingImpersonation",
+        "tools.ozone.report.defs#reasonMisleadingSpam",
+        "tools.ozone.report.defs#reasonMisleadingScam",
+        "tools.ozone.report.defs#reasonMisleadingElections",
+        "tools.ozone.report.defs#reasonMisleadingOther",
+
+        "tools.ozone.report.defs#reasonRuleSiteSecurity",
+        "tools.ozone.report.defs#reasonRuleProhibitedSales",
+        "tools.ozone.report.defs#reasonRuleBanEvasion",
+        "tools.ozone.report.defs#reasonRuleOther",
+
+        "tools.ozone.report.defs#reasonSelfHarmContent",
+        "tools.ozone.report.defs#reasonSelfHarmED",
+        "tools.ozone.report.defs#reasonSelfHarmStunts",
+        "tools.ozone.report.defs#reasonSelfHarmSubstances",
+        "tools.ozone.report.defs#reasonSelfHarmOther"
+      ]
+    },
+    "reasonAppeal": {
+      "type": "token",
+      "description": "Appeal a previously taken moderation action"
+    },
+    "reasonOther": {
+      "type": "token",
+      "description": "An issue not included in these options"
+    },
+    "reasonViolenceAnimal": {
+      "type": "token",
+      "description": "Animal welfare violations"
+    },
+    "reasonViolenceThreats": {
+      "type": "token",
+      "description": "Threats or incitement"
+    },
+    "reasonViolenceGraphicContent": {
+      "type": "token",
+      "description": "Graphic violent content"
+    },
+    "reasonViolenceGlorification": {
+      "type": "token",
+      "description": "Glorification of violence"
+    },
+    "reasonViolenceExtremistContent": {
+      "type": "token",
+      "description": "Extremist content. These reports will be sent only be sent to the application's Moderation Authority."
+    },
+    "reasonViolenceTrafficking": {
+      "type": "token",
+      "description": "Human trafficking"
+    },
+    "reasonViolenceOther": {
+      "type": "token",
+      "description": "Other violent content"
+    },
+
+    "reasonSexualAbuseContent": {
+      "type": "token",
+      "description": "Adult sexual abuse content"
+    },
+    "reasonSexualNCII": {
+      "type": "token",
+      "description": "Non-consensual intimate imagery"
+    },
+    "reasonSexualDeepfake": {
+      "type": "token",
+      "description": "Deepfake adult content"
+    },
+    "reasonSexualAnimal": {
+      "type": "token",
+      "description": "Animal sexual abuse"
+    },
+    "reasonSexualUnlabeled": {
+      "type": "token",
+      "description": "Unlabelled adult content"
+    },
+    "reasonSexualOther": {
+      "type": "token",
+      "description": "Other sexual violence content"
+    },
+
+    "reasonChildSafetyCSAM": {
+      "type": "token",
+      "description": "Child sexual abuse material (CSAM). These reports will be sent only be sent to the application's Moderation Authority."
+    },
+    "reasonChildSafetyGroom": {
+      "type": "token",
+      "description": "Grooming or predatory behavior. These reports will be sent only be sent to the application's Moderation Authority."
+    },
+    "reasonChildSafetyPrivacy": {
+      "type": "token",
+      "description": "Privacy violation involving a minor"
+    },
+    "reasonChildSafetyHarassment": {
+      "type": "token",
+      "description": "Harassment or bullying of minors"
+    },
+    "reasonChildSafetyOther": {
+      "type": "token",
+      "description": "Other child safety. These reports will be sent only be sent to the application's Moderation Authority."
+    },
+
+    "reasonHarassmentTroll": {
+      "type": "token",
+      "description": "Trolling"
+    },
+    "reasonHarassmentTargeted": {
+      "type": "token",
+      "description": "Targeted harassment"
+    },
+    "reasonHarassmentHateSpeech": {
+      "type": "token",
+      "description": "Hate speech"
+    },
+    "reasonHarassmentDoxxing": {
+      "type": "token",
+      "description": "Doxxing"
+    },
+    "reasonHarassmentOther": {
+      "type": "token",
+      "description": "Other harassing or hateful content"
+    },
+
+    "reasonMisleadingBot": {
+      "type": "token",
+      "description": "Fake account or bot"
+    },
+    "reasonMisleadingImpersonation": {
+      "type": "token",
+      "description": "Impersonation"
+    },
+    "reasonMisleadingSpam": {
+      "type": "token",
+      "description": "Spam"
+    },
+    "reasonMisleadingScam": {
+      "type": "token",
+      "description": "Scam"
+    },
+    "reasonMisleadingElections": {
+      "type": "token",
+      "description": "False information about elections"
+    },
+    "reasonMisleadingOther": {
+      "type": "token",
+      "description": "Other misleading content"
+    },
+
+    "reasonRuleSiteSecurity": {
+      "type": "token",
+      "description": "Hacking or system attacks"
+    },
+    "reasonRuleProhibitedSales": {
+      "type": "token",
+      "description": "Promoting or selling prohibited items or services"
+    },
+    "reasonRuleBanEvasion": {
+      "type": "token",
+      "description": "Banned user returning"
+    },
+    "reasonRuleOther": {
+      "type": "token",
+      "description": "Other"
+    },
+    "reasonSelfHarmContent": {
+      "type": "token",
+      "description": "Content promoting or depicting self-harm"
+    },
+    "reasonSelfHarmED": {
+      "type": "token",
+      "description": "Eating disorders"
+    },
+    "reasonSelfHarmStunts": {
+      "type": "token",
+      "description": "Dangerous challenges or activities"
+    },
+    "reasonSelfHarmSubstances": {
+      "type": "token",
+      "description": "Dangerous substances or drug abuse"
+    },
+    "reasonSelfHarmOther": {
+      "type": "token",
+      "description": "Other dangerous content"
+    }
+  }
+}
diff --git a/lexicons/tools/ozone/safelink/addRule.json b/lexicons/tools/ozone/safelink/addRule.json
new file mode 100644
index 00000000..610d9183
--- /dev/null
+++ b/lexicons/tools/ozone/safelink/addRule.json
@@ -0,0 +1,61 @@
+{
+  "lexicon": 1,
+  "id": "tools.ozone.safelink.addRule",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Add a new URL safety rule",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["url", "pattern", "action", "reason"],
+          "properties": {
+            "url": {
+              "type": "string",
+              "description": "The URL or domain to apply the rule to"
+            },
+            "pattern": {
+              "type": "ref",
+              "ref": "tools.ozone.safelink.defs#patternType"
+            },
+            "action": {
+              "type": "ref",
+              "ref": "tools.ozone.safelink.defs#actionType"
+            },
+            "reason": {
+              "type": "ref",
+              "ref": "tools.ozone.safelink.defs#reasonType"
+            },
+            "comment": {
+              "type": "string",
+              "description": "Optional comment about the decision"
+            },
+            "createdBy": {
+              "type": "string",
+              "format": "did",
+              "description": "Author DID. Only respected when using admin auth"
+            }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "ref",
+          "ref": "tools.ozone.safelink.defs#event"
+        }
+      },
+      "errors": [
+        {
+          "name": "InvalidUrl",
+          "description": "The provided URL is invalid"
+        },
+        {
+          "name": "RuleAlreadyExists",
+          "description": "A rule for this URL/domain already exists"
+        }
+      ]
+    }
+  }
+}
diff --git a/lexicons/tools/ozone/safelink/defs.json b/lexicons/tools/ozone/safelink/defs.json
new file mode 100644
index 00000000..135653a1
--- /dev/null
+++ b/lexicons/tools/ozone/safelink/defs.json
@@ -0,0 +1,125 @@
+{
+  "lexicon": 1,
+  "id": "tools.ozone.safelink.defs",
+  "defs": {
+    "event": {
+      "type": "object",
+      "description": "An event for URL safety decisions",
+      "required": [
+        "id",
+        "eventType",
+        "url",
+        "pattern",
+        "action",
+        "reason",
+        "createdBy",
+        "createdAt"
+      ],
+      "properties": {
+        "id": {
+          "type": "integer",
+          "description": "Auto-incrementing row ID"
+        },
+        "eventType": {
+          "type": "ref",
+          "ref": "#eventType"
+        },
+        "url": {
+          "type": "string",
+          "description": "The URL that this rule applies to"
+        },
+        "pattern": {
+          "type": "ref",
+          "ref": "#patternType"
+        },
+        "action": {
+          "type": "ref",
+          "ref": "#actionType"
+        },
+        "reason": {
+          "type": "ref",
+          "ref": "#reasonType"
+        },
+        "createdBy": {
+          "type": "string",
+          "format": "did",
+          "description": "DID of the user who created this rule"
+        },
+        "createdAt": {
+          "type": "string",
+          "format": "datetime"
+        },
+        "comment": {
+          "type": "string",
+          "description": "Optional comment about the decision"
+        }
+      }
+    },
+    "eventType": {
+      "type": "string",
+      "knownValues": ["addRule", "updateRule", "removeRule"]
+    },
+    "patternType": {
+      "type": "string",
+      "knownValues": ["domain", "url"]
+    },
+    "actionType": {
+      "type": "string",
+      "knownValues": ["block", "warn", "whitelist"]
+    },
+    "reasonType": {
+      "type": "string",
+      "knownValues": ["csam", "spam", "phishing", "none"]
+    },
+    "urlRule": {
+      "type": "object",
+      "description": "Input for creating a URL safety rule",
+      "required": [
+        "url",
+        "pattern",
+        "action",
+        "reason",
+        "createdBy",
+        "createdAt",
+        "updatedAt"
+      ],
+      "properties": {
+        "url": {
+          "type": "string",
+          "description": "The URL or domain to apply the rule to"
+        },
+        "pattern": {
+          "type": "ref",
+          "ref": "#patternType"
+        },
+        "action": {
+          "type": "ref",
+          "ref": "#actionType"
+        },
+        "reason": {
+          "type": "ref",
+          "ref": "#reasonType"
+        },
+        "comment": {
+          "type": "string",
+          "description": "Optional comment about the decision"
+        },
+        "createdBy": {
+          "type": "string",
+          "format": "did",
+          "description": "DID of the user added the rule."
+        },
+        "createdAt": {
+          "type": "string",
+          "format": "datetime",
+          "description": "Timestamp when the rule was created"
+        },
+        "updatedAt": {
+          "type": "string",
+          "format": "datetime",
+          "description": "Timestamp when the rule was last updated"
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/tools/ozone/safelink/queryEvents.json b/lexicons/tools/ozone/safelink/queryEvents.json
new file mode 100644
index 00000000..9d00644c
--- /dev/null
+++ b/lexicons/tools/ozone/safelink/queryEvents.json
@@ -0,0 +1,66 @@
+{
+  "lexicon": 1,
+  "id": "tools.ozone.safelink.queryEvents",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Query URL safety audit events",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "properties": {
+            "cursor": {
+              "type": "string",
+              "description": "Cursor for pagination"
+            },
+            "limit": {
+              "type": "integer",
+              "minimum": 1,
+              "maximum": 100,
+              "default": 50,
+              "description": "Maximum number of results to return"
+            },
+            "urls": {
+              "type": "array",
+              "items": {
+                "type": "string"
+              },
+              "description": "Filter by specific URLs or domains"
+            },
+            "patternType": {
+              "type": "string",
+              "description": "Filter by pattern type"
+            },
+            "sortDirection": {
+              "type": "string",
+              "knownValues": ["asc", "desc"],
+              "default": "desc",
+              "description": "Sort direction"
+            }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["events"],
+          "properties": {
+            "cursor": {
+              "type": "string",
+              "description": "Next cursor for pagination. Only present if there are more results."
+            },
+            "events": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "tools.ozone.safelink.defs#event"
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/tools/ozone/safelink/queryRules.json b/lexicons/tools/ozone/safelink/queryRules.json
new file mode 100644
index 00000000..2b41d463
--- /dev/null
+++ b/lexicons/tools/ozone/safelink/queryRules.json
@@ -0,0 +1,82 @@
+{
+  "lexicon": 1,
+  "id": "tools.ozone.safelink.queryRules",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Query URL safety rules",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "properties": {
+            "cursor": {
+              "type": "string",
+              "description": "Cursor for pagination"
+            },
+            "limit": {
+              "type": "integer",
+              "minimum": 1,
+              "maximum": 100,
+              "default": 50,
+              "description": "Maximum number of results to return"
+            },
+            "urls": {
+              "type": "array",
+              "items": {
+                "type": "string"
+              },
+              "description": "Filter by specific URLs or domains"
+            },
+            "patternType": {
+              "type": "string",
+              "description": "Filter by pattern type"
+            },
+            "actions": {
+              "type": "array",
+              "items": {
+                "type": "string"
+              },
+              "description": "Filter by action types"
+            },
+            "reason": {
+              "type": "string",
+              "description": "Filter by reason type"
+            },
+            "createdBy": {
+              "type": "string",
+              "format": "did",
+              "description": "Filter by rule creator"
+            },
+            "sortDirection": {
+              "type": "string",
+              "knownValues": ["asc", "desc"],
+              "default": "desc",
+              "description": "Sort direction"
+            }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["rules"],
+          "properties": {
+            "cursor": {
+              "type": "string",
+              "description": "Next cursor for pagination. Only present if there are more results."
+            },
+            "rules": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "tools.ozone.safelink.defs#urlRule"
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/tools/ozone/safelink/removeRule.json b/lexicons/tools/ozone/safelink/removeRule.json
new file mode 100644
index 00000000..2a9ebef8
--- /dev/null
+++ b/lexicons/tools/ozone/safelink/removeRule.json
@@ -0,0 +1,49 @@
+{
+  "lexicon": 1,
+  "id": "tools.ozone.safelink.removeRule",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Remove an existing URL safety rule",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["url", "pattern"],
+          "properties": {
+            "url": {
+              "type": "string",
+              "description": "The URL or domain to remove the rule for"
+            },
+            "pattern": {
+              "type": "ref",
+              "ref": "tools.ozone.safelink.defs#patternType"
+            },
+            "comment": {
+              "type": "string",
+              "description": "Optional comment about why the rule is being removed"
+            },
+            "createdBy": {
+              "type": "string",
+              "format": "did",
+              "description": "Optional DID of the user. Only respected when using admin auth."
+            }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "ref",
+          "ref": "tools.ozone.safelink.defs#event"
+        }
+      },
+      "errors": [
+        {
+          "name": "RuleNotFound",
+          "description": "No active rule found for this URL/domain"
+        }
+      ]
+    }
+  }
+}
diff --git a/lexicons/tools/ozone/safelink/updateRule.json b/lexicons/tools/ozone/safelink/updateRule.json
new file mode 100644
index 00000000..719304c6
--- /dev/null
+++ b/lexicons/tools/ozone/safelink/updateRule.json
@@ -0,0 +1,57 @@
+{
+  "lexicon": 1,
+  "id": "tools.ozone.safelink.updateRule",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Update an existing URL safety rule",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["url", "pattern", "action", "reason"],
+          "properties": {
+            "url": {
+              "type": "string",
+              "description": "The URL or domain to update the rule for"
+            },
+            "pattern": {
+              "type": "ref",
+              "ref": "tools.ozone.safelink.defs#patternType"
+            },
+            "action": {
+              "type": "ref",
+              "ref": "tools.ozone.safelink.defs#actionType"
+            },
+            "reason": {
+              "type": "ref",
+              "ref": "tools.ozone.safelink.defs#reasonType"
+            },
+            "comment": {
+              "type": "string",
+              "description": "Optional comment about the update"
+            },
+            "createdBy": {
+              "type": "string",
+              "format": "did",
+              "description": "Optional DID to credit as the creator. Only respected for admin_token authentication."
+            }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "ref",
+          "ref": "tools.ozone.safelink.defs#event"
+        }
+      },
+      "errors": [
+        {
+          "name": "RuleNotFound",
+          "description": "No active rule found for this URL/domain"
+        }
+      ]
+    }
+  }
+}
diff --git a/lexicons/tools/ozone/server/getConfig.json b/lexicons/tools/ozone/server/getConfig.json
new file mode 100644
index 00000000..c156d6f4
--- /dev/null
+++ b/lexicons/tools/ozone/server/getConfig.json
@@ -0,0 +1,66 @@
+{
+  "lexicon": 1,
+  "id": "tools.ozone.server.getConfig",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get details about ozone's server configuration.",
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "properties": {
+            "appview": {
+              "type": "ref",
+              "ref": "#serviceConfig"
+            },
+            "pds": {
+              "type": "ref",
+              "ref": "#serviceConfig"
+            },
+            "blobDivert": {
+              "type": "ref",
+              "ref": "#serviceConfig"
+            },
+            "chat": {
+              "type": "ref",
+              "ref": "#serviceConfig"
+            },
+            "viewer": {
+              "type": "ref",
+              "ref": "#viewerConfig"
+            },
+            "verifierDid": {
+              "type": "string",
+              "format": "did",
+              "description": "The did of the verifier used for verification."
+            }
+          }
+        }
+      }
+    },
+    "serviceConfig": {
+      "type": "object",
+      "properties": {
+        "url": {
+          "type": "string",
+          "format": "uri"
+        }
+      }
+    },
+    "viewerConfig": {
+      "type": "object",
+      "properties": {
+        "role": {
+          "type": "string",
+          "knownValues": [
+            "tools.ozone.team.defs#roleAdmin",
+            "tools.ozone.team.defs#roleModerator",
+            "tools.ozone.team.defs#roleTriage",
+            "tools.ozone.team.defs#roleVerifier"
+          ]
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/tools/ozone/set/addValues.json b/lexicons/tools/ozone/set/addValues.json
new file mode 100644
index 00000000..345df5db
--- /dev/null
+++ b/lexicons/tools/ozone/set/addValues.json
@@ -0,0 +1,32 @@
+{
+  "lexicon": 1,
+  "id": "tools.ozone.set.addValues",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Add values to a specific set. Attempting to add values to a set that does not exist will result in an error.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["name", "values"],
+          "properties": {
+            "name": {
+              "type": "string",
+              "description": "Name of the set to add values to"
+            },
+            "values": {
+              "type": "array",
+              "minLength": 1,
+              "maxLength": 1000,
+              "items": {
+                "type": "string"
+              },
+              "description": "Array of string values to add to the set"
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/tools/ozone/set/defs.json b/lexicons/tools/ozone/set/defs.json
new file mode 100644
index 00000000..4ad69521
--- /dev/null
+++ b/lexicons/tools/ozone/set/defs.json
@@ -0,0 +1,49 @@
+{
+  "lexicon": 1,
+  "id": "tools.ozone.set.defs",
+  "defs": {
+    "set": {
+      "type": "object",
+      "required": ["name"],
+      "properties": {
+        "name": {
+          "type": "string",
+          "minLength": 3,
+          "maxLength": 128
+        },
+        "description": {
+          "type": "string",
+          "maxGraphemes": 1024,
+          "maxLength": 10240
+        }
+      }
+    },
+    "setView": {
+      "type": "object",
+      "required": ["name", "setSize", "createdAt", "updatedAt"],
+      "properties": {
+        "name": {
+          "type": "string",
+          "minLength": 3,
+          "maxLength": 128
+        },
+        "description": {
+          "type": "string",
+          "maxGraphemes": 1024,
+          "maxLength": 10240
+        },
+        "setSize": {
+          "type": "integer"
+        },
+        "createdAt": {
+          "type": "string",
+          "format": "datetime"
+        },
+        "updatedAt": {
+          "type": "string",
+          "format": "datetime"
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/tools/ozone/set/deleteSet.json b/lexicons/tools/ozone/set/deleteSet.json
new file mode 100644
index 00000000..70e60003
--- /dev/null
+++ b/lexicons/tools/ozone/set/deleteSet.json
@@ -0,0 +1,36 @@
+{
+  "lexicon": 1,
+  "id": "tools.ozone.set.deleteSet",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Delete an entire set. Attempting to delete a set that does not exist will result in an error.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["name"],
+          "properties": {
+            "name": {
+              "type": "string",
+              "description": "Name of the set to delete"
+            }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "properties": {}
+        }
+      },
+      "errors": [
+        {
+          "name": "SetNotFound",
+          "description": "set with the given name does not exist"
+        }
+      ]
+    }
+  }
+}
diff --git a/lexicons/tools/ozone/set/deleteValues.json b/lexicons/tools/ozone/set/deleteValues.json
new file mode 100644
index 00000000..e40b8d44
--- /dev/null
+++ b/lexicons/tools/ozone/set/deleteValues.json
@@ -0,0 +1,37 @@
+{
+  "lexicon": 1,
+  "id": "tools.ozone.set.deleteValues",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Delete values from a specific set. Attempting to delete values that are not in the set will not result in an error",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["name", "values"],
+          "properties": {
+            "name": {
+              "type": "string",
+              "description": "Name of the set to delete values from"
+            },
+            "values": {
+              "type": "array",
+              "minLength": 1,
+              "items": {
+                "type": "string"
+              },
+              "description": "Array of string values to delete from the set"
+            }
+          }
+        }
+      },
+      "errors": [
+        {
+          "name": "SetNotFound",
+          "description": "set with the given name does not exist"
+        }
+      ]
+    }
+  }
+}
diff --git a/lexicons/tools/ozone/set/getValues.json b/lexicons/tools/ozone/set/getValues.json
new file mode 100644
index 00000000..dca2fb27
--- /dev/null
+++ b/lexicons/tools/ozone/set/getValues.json
@@ -0,0 +1,56 @@
+{
+  "lexicon": 1,
+  "id": "tools.ozone.set.getValues",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get a specific set and its values",
+      "parameters": {
+        "type": "params",
+        "required": ["name"],
+        "properties": {
+          "name": {
+            "type": "string"
+          },
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 1000,
+            "default": 100
+          },
+          "cursor": {
+            "type": "string"
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["set", "values"],
+          "properties": {
+            "set": {
+              "type": "ref",
+              "ref": "tools.ozone.set.defs#setView"
+            },
+            "values": {
+              "type": "array",
+              "items": {
+                "type": "string"
+              }
+            },
+            "cursor": {
+              "type": "string"
+            }
+          }
+        }
+      },
+      "errors": [
+        {
+          "name": "SetNotFound",
+          "description": "set with the given name does not exist"
+        }
+      ]
+    }
+  }
+}
diff --git a/lexicons/tools/ozone/set/querySets.json b/lexicons/tools/ozone/set/querySets.json
new file mode 100644
index 00000000..d4bedb00
--- /dev/null
+++ b/lexicons/tools/ozone/set/querySets.json
@@ -0,0 +1,57 @@
+{
+  "lexicon": 1,
+  "id": "tools.ozone.set.querySets",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Query available sets",
+      "parameters": {
+        "type": "params",
+        "properties": {
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 100,
+            "default": 50
+          },
+          "cursor": {
+            "type": "string"
+          },
+          "namePrefix": {
+            "type": "string"
+          },
+          "sortBy": {
+            "type": "string",
+            "enum": ["name", "createdAt", "updatedAt"],
+            "default": "name"
+          },
+          "sortDirection": {
+            "type": "string",
+            "default": "asc",
+            "enum": ["asc", "desc"],
+            "description": "Defaults to ascending order of name field."
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["sets"],
+          "properties": {
+            "sets": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "tools.ozone.set.defs#setView"
+              }
+            },
+            "cursor": {
+              "type": "string"
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/tools/ozone/set/upsertSet.json b/lexicons/tools/ozone/set/upsertSet.json
new file mode 100644
index 00000000..43ae0f95
--- /dev/null
+++ b/lexicons/tools/ozone/set/upsertSet.json
@@ -0,0 +1,24 @@
+{
+  "lexicon": 1,
+  "id": "tools.ozone.set.upsertSet",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Create or update set metadata",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "ref",
+          "ref": "tools.ozone.set.defs#set"
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "ref",
+          "ref": "tools.ozone.set.defs#setView"
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/tools/ozone/setting/defs.json b/lexicons/tools/ozone/setting/defs.json
new file mode 100644
index 00000000..57d325fd
--- /dev/null
+++ b/lexicons/tools/ozone/setting/defs.json
@@ -0,0 +1,64 @@
+{
+  "lexicon": 1,
+  "id": "tools.ozone.setting.defs",
+  "defs": {
+    "option": {
+      "type": "object",
+      "required": [
+        "key",
+        "value",
+        "did",
+        "scope",
+        "createdBy",
+        "lastUpdatedBy"
+      ],
+      "properties": {
+        "key": {
+          "type": "string",
+          "format": "nsid"
+        },
+        "did": {
+          "type": "string",
+          "format": "did"
+        },
+        "value": {
+          "type": "unknown"
+        },
+        "description": {
+          "type": "string",
+          "maxGraphemes": 1024,
+          "maxLength": 10240
+        },
+        "createdAt": {
+          "type": "string",
+          "format": "datetime"
+        },
+        "updatedAt": {
+          "type": "string",
+          "format": "datetime"
+        },
+        "managerRole": {
+          "type": "string",
+          "knownValues": [
+            "tools.ozone.team.defs#roleModerator",
+            "tools.ozone.team.defs#roleTriage",
+            "tools.ozone.team.defs#roleAdmin",
+            "tools.ozone.team.defs#roleVerifier"
+          ]
+        },
+        "scope": {
+          "type": "string",
+          "knownValues": ["instance", "personal"]
+        },
+        "createdBy": {
+          "type": "string",
+          "format": "did"
+        },
+        "lastUpdatedBy": {
+          "type": "string",
+          "format": "did"
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/tools/ozone/setting/listOptions.json b/lexicons/tools/ozone/setting/listOptions.json
new file mode 100644
index 00000000..849b9c1d
--- /dev/null
+++ b/lexicons/tools/ozone/setting/listOptions.json
@@ -0,0 +1,61 @@
+{
+  "lexicon": 1,
+  "id": "tools.ozone.setting.listOptions",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "List settings with optional filtering",
+      "parameters": {
+        "type": "params",
+        "properties": {
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 100,
+            "default": 50
+          },
+          "cursor": {
+            "type": "string"
+          },
+          "scope": {
+            "type": "string",
+            "knownValues": ["instance", "personal"],
+            "default": "instance"
+          },
+          "prefix": {
+            "type": "string",
+            "description": "Filter keys by prefix"
+          },
+          "keys": {
+            "type": "array",
+            "maxLength": 100,
+            "items": {
+              "type": "string",
+              "format": "nsid"
+            },
+            "description": "Filter for only the specified keys. Ignored if prefix is provided"
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["options"],
+          "properties": {
+            "cursor": {
+              "type": "string"
+            },
+            "options": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "tools.ozone.setting.defs#option"
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/tools/ozone/setting/removeOptions.json b/lexicons/tools/ozone/setting/removeOptions.json
new file mode 100644
index 00000000..ae55d971
--- /dev/null
+++ b/lexicons/tools/ozone/setting/removeOptions.json
@@ -0,0 +1,39 @@
+{
+  "lexicon": 1,
+  "id": "tools.ozone.setting.removeOptions",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Delete settings by key",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["keys", "scope"],
+          "properties": {
+            "keys": {
+              "type": "array",
+              "minLength": 1,
+              "maxLength": 200,
+              "items": {
+                "type": "string",
+                "format": "nsid"
+              }
+            },
+            "scope": {
+              "type": "string",
+              "knownValues": ["instance", "personal"]
+            }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "properties": {}
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/tools/ozone/setting/upsertOption.json b/lexicons/tools/ozone/setting/upsertOption.json
new file mode 100644
index 00000000..43f1c935
--- /dev/null
+++ b/lexicons/tools/ozone/setting/upsertOption.json
@@ -0,0 +1,56 @@
+{
+  "lexicon": 1,
+  "id": "tools.ozone.setting.upsertOption",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Create or update setting option",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["key", "scope", "value"],
+          "properties": {
+            "key": {
+              "type": "string",
+              "format": "nsid"
+            },
+            "scope": {
+              "type": "string",
+              "knownValues": ["instance", "personal"]
+            },
+            "value": {
+              "type": "unknown"
+            },
+            "description": {
+              "type": "string",
+              "maxLength": 2000
+            },
+            "managerRole": {
+              "type": "string",
+              "knownValues": [
+                "tools.ozone.team.defs#roleModerator",
+                "tools.ozone.team.defs#roleTriage",
+                "tools.ozone.team.defs#roleVerifier",
+                "tools.ozone.team.defs#roleAdmin"
+              ]
+            }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["option"],
+          "properties": {
+            "option": {
+              "type": "ref",
+              "ref": "tools.ozone.setting.defs#option"
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/tools/ozone/signature/defs.json b/lexicons/tools/ozone/signature/defs.json
new file mode 100644
index 00000000..6c7297eb
--- /dev/null
+++ b/lexicons/tools/ozone/signature/defs.json
@@ -0,0 +1,14 @@
+{
+  "lexicon": 1,
+  "id": "tools.ozone.signature.defs",
+  "defs": {
+    "sigDetail": {
+      "type": "object",
+      "required": ["property", "value"],
+      "properties": {
+        "property": { "type": "string" },
+        "value": { "type": "string" }
+      }
+    }
+  }
+}
diff --git a/lexicons/tools/ozone/signature/findCorrelation.json b/lexicons/tools/ozone/signature/findCorrelation.json
new file mode 100644
index 00000000..f803f25c
--- /dev/null
+++ b/lexicons/tools/ozone/signature/findCorrelation.json
@@ -0,0 +1,39 @@
+{
+  "lexicon": 1,
+  "id": "tools.ozone.signature.findCorrelation",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Find all correlated threat signatures between 2 or more accounts.",
+      "parameters": {
+        "type": "params",
+        "required": ["dids"],
+        "properties": {
+          "dids": {
+            "type": "array",
+            "items": {
+              "type": "string",
+              "format": "did"
+            }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["details"],
+          "properties": {
+            "details": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "tools.ozone.signature.defs#sigDetail"
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/tools/ozone/signature/findRelatedAccounts.json b/lexicons/tools/ozone/signature/findRelatedAccounts.json
new file mode 100644
index 00000000..f5e4c7b0
--- /dev/null
+++ b/lexicons/tools/ozone/signature/findRelatedAccounts.json
@@ -0,0 +1,61 @@
+{
+  "lexicon": 1,
+  "id": "tools.ozone.signature.findRelatedAccounts",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Get accounts that share some matching threat signatures with the root account.",
+      "parameters": {
+        "type": "params",
+        "required": ["did"],
+        "properties": {
+          "did": {
+            "type": "string",
+            "format": "did"
+          },
+          "cursor": { "type": "string" },
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 100,
+            "default": 50
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["accounts"],
+          "properties": {
+            "cursor": { "type": "string" },
+            "accounts": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "#relatedAccount"
+              }
+            }
+          }
+        }
+      }
+    },
+    "relatedAccount": {
+      "type": "object",
+      "required": ["account"],
+      "properties": {
+        "account": {
+          "type": "ref",
+          "ref": "com.atproto.admin.defs#accountView"
+        },
+        "similarities": {
+          "type": "array",
+          "items": {
+            "type": "ref",
+            "ref": "tools.ozone.signature.defs#sigDetail"
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/tools/ozone/signature/searchAccounts.json b/lexicons/tools/ozone/signature/searchAccounts.json
new file mode 100644
index 00000000..328d7787
--- /dev/null
+++ b/lexicons/tools/ozone/signature/searchAccounts.json
@@ -0,0 +1,46 @@
+{
+  "lexicon": 1,
+  "id": "tools.ozone.signature.searchAccounts",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "Search for accounts that match one or more threat signature values.",
+      "parameters": {
+        "type": "params",
+        "required": ["values"],
+        "properties": {
+          "values": {
+            "type": "array",
+            "items": {
+              "type": "string"
+            }
+          },
+          "cursor": { "type": "string" },
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 100,
+            "default": 50
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["accounts"],
+          "properties": {
+            "cursor": { "type": "string" },
+            "accounts": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "com.atproto.admin.defs#accountView"
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/tools/ozone/team/addMember.json b/lexicons/tools/ozone/team/addMember.json
new file mode 100644
index 00000000..90b8f947
--- /dev/null
+++ b/lexicons/tools/ozone/team/addMember.json
@@ -0,0 +1,42 @@
+{
+  "lexicon": 1,
+  "id": "tools.ozone.team.addMember",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Add a member to the ozone team. Requires admin role.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["did", "role"],
+          "properties": {
+            "did": { "type": "string", "format": "did" },
+            "role": {
+              "type": "string",
+              "knownValues": [
+                "tools.ozone.team.defs#roleAdmin",
+                "tools.ozone.team.defs#roleModerator",
+                "tools.ozone.team.defs#roleVerifier",
+                "tools.ozone.team.defs#roleTriage"
+              ]
+            }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "ref",
+          "ref": "tools.ozone.team.defs#member"
+        }
+      },
+      "errors": [
+        {
+          "name": "MemberAlreadyExists",
+          "description": "Member already exists in the team."
+        }
+      ]
+    }
+  }
+}
diff --git a/lexicons/tools/ozone/team/defs.json b/lexicons/tools/ozone/team/defs.json
new file mode 100644
index 00000000..a1c378dd
--- /dev/null
+++ b/lexicons/tools/ozone/team/defs.json
@@ -0,0 +1,46 @@
+{
+  "lexicon": 1,
+  "id": "tools.ozone.team.defs",
+  "defs": {
+    "member": {
+      "type": "object",
+      "required": ["did", "role"],
+      "properties": {
+        "did": { "type": "string", "format": "did" },
+        "disabled": { "type": "boolean" },
+        "profile": {
+          "type": "ref",
+          "ref": "app.bsky.actor.defs#profileViewDetailed"
+        },
+        "createdAt": { "type": "string", "format": "datetime" },
+        "updatedAt": { "type": "string", "format": "datetime" },
+        "lastUpdatedBy": { "type": "string" },
+        "role": {
+          "type": "string",
+          "knownValues": [
+            "#roleAdmin",
+            "#roleModerator",
+            "#roleTriage",
+            "#roleVerifier"
+          ]
+        }
+      }
+    },
+    "roleAdmin": {
+      "type": "token",
+      "description": "Admin role. Highest level of access, can perform all actions."
+    },
+    "roleModerator": {
+      "type": "token",
+      "description": "Moderator role. Can perform most actions."
+    },
+    "roleTriage": {
+      "type": "token",
+      "description": "Triage role. Mostly intended for monitoring and escalating issues."
+    },
+    "roleVerifier": {
+      "type": "token",
+      "description": "Verifier role. Only allowed to issue verifications."
+    }
+  }
+}
diff --git a/lexicons/tools/ozone/team/deleteMember.json b/lexicons/tools/ozone/team/deleteMember.json
new file mode 100644
index 00000000..a9008c9e
--- /dev/null
+++ b/lexicons/tools/ozone/team/deleteMember.json
@@ -0,0 +1,30 @@
+{
+  "lexicon": 1,
+  "id": "tools.ozone.team.deleteMember",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Delete a member from ozone team. Requires admin role.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["did"],
+          "properties": {
+            "did": { "type": "string", "format": "did" }
+          }
+        }
+      },
+      "errors": [
+        {
+          "name": "MemberNotFound",
+          "description": "The member being deleted does not exist"
+        },
+        {
+          "name": "CannotDeleteSelf",
+          "description": "You can not delete yourself from the team"
+        }
+      ]
+    }
+  }
+}
diff --git a/lexicons/tools/ozone/team/listMembers.json b/lexicons/tools/ozone/team/listMembers.json
new file mode 100644
index 00000000..3f4ce322
--- /dev/null
+++ b/lexicons/tools/ozone/team/listMembers.json
@@ -0,0 +1,55 @@
+{
+  "lexicon": 1,
+  "id": "tools.ozone.team.listMembers",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "List all members with access to the ozone service.",
+      "parameters": {
+        "type": "params",
+        "properties": {
+          "q": {
+            "type": "string"
+          },
+          "disabled": {
+            "type": "boolean"
+          },
+          "roles": {
+            "type": "array",
+            "items": {
+              "type": "string"
+            }
+          },
+          "limit": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 100,
+            "default": 50
+          },
+          "cursor": {
+            "type": "string"
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["members"],
+          "properties": {
+            "cursor": {
+              "type": "string"
+            },
+            "members": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "tools.ozone.team.defs#member"
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/tools/ozone/team/updateMember.json b/lexicons/tools/ozone/team/updateMember.json
new file mode 100644
index 00000000..839194ea
--- /dev/null
+++ b/lexicons/tools/ozone/team/updateMember.json
@@ -0,0 +1,43 @@
+{
+  "lexicon": 1,
+  "id": "tools.ozone.team.updateMember",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Update a member in the ozone service. Requires admin role.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["did"],
+          "properties": {
+            "did": { "type": "string", "format": "did" },
+            "disabled": { "type": "boolean" },
+            "role": {
+              "type": "string",
+              "knownValues": [
+                "tools.ozone.team.defs#roleAdmin",
+                "tools.ozone.team.defs#roleModerator",
+                "tools.ozone.team.defs#roleVerifier",
+                "tools.ozone.team.defs#roleTriage"
+              ]
+            }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "ref",
+          "ref": "tools.ozone.team.defs#member"
+        }
+      },
+      "errors": [
+        {
+          "name": "MemberNotFound",
+          "description": "The member being updated does not exist in the team"
+        }
+      ]
+    }
+  }
+}
diff --git a/lexicons/tools/ozone/verification/defs.json b/lexicons/tools/ozone/verification/defs.json
new file mode 100644
index 00000000..c462eb5e
--- /dev/null
+++ b/lexicons/tools/ozone/verification/defs.json
@@ -0,0 +1,85 @@
+{
+  "lexicon": 1,
+  "id": "tools.ozone.verification.defs",
+  "defs": {
+    "verificationView": {
+      "type": "object",
+      "description": "Verification data for the associated subject.",
+      "required": [
+        "issuer",
+        "uri",
+        "subject",
+        "handle",
+        "displayName",
+        "createdAt"
+      ],
+      "properties": {
+        "issuer": {
+          "type": "string",
+          "description": "The user who issued this verification.",
+          "format": "did"
+        },
+        "uri": {
+          "type": "string",
+          "description": "The AT-URI of the verification record.",
+          "format": "at-uri"
+        },
+        "subject": {
+          "type": "string",
+          "format": "did",
+          "description": "The subject of the verification."
+        },
+        "handle": {
+          "type": "string",
+          "description": "Handle of the subject the verification applies to at the moment of verifying, which might not be the same at the time of viewing. The verification is only valid if the current handle matches the one at the time of verifying.",
+          "format": "handle"
+        },
+        "displayName": {
+          "type": "string",
+          "description": "Display name of the subject the verification applies to at the moment of verifying, which might not be the same at the time of viewing. The verification is only valid if the current displayName matches the one at the time of verifying."
+        },
+        "createdAt": {
+          "type": "string",
+          "description": "Timestamp when the verification was created.",
+          "format": "datetime"
+        },
+        "revokeReason": {
+          "type": "string",
+          "description": "Describes the reason for revocation, also indicating that the verification is no longer valid."
+        },
+        "revokedAt": {
+          "type": "string",
+          "description": "Timestamp when the verification was revoked.",
+          "format": "datetime"
+        },
+        "revokedBy": {
+          "type": "string",
+          "description": "The user who revoked this verification.",
+          "format": "did"
+        },
+        "subjectProfile": {
+          "type": "union",
+          "refs": []
+        },
+        "issuerProfile": {
+          "type": "union",
+          "refs": []
+        },
+        "subjectRepo": {
+          "type": "union",
+          "refs": [
+            "tools.ozone.moderation.defs#repoViewDetail",
+            "tools.ozone.moderation.defs#repoViewNotFound"
+          ]
+        },
+        "issuerRepo": {
+          "type": "union",
+          "refs": [
+            "tools.ozone.moderation.defs#repoViewDetail",
+            "tools.ozone.moderation.defs#repoViewNotFound"
+          ]
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/tools/ozone/verification/grantVerifications.json b/lexicons/tools/ozone/verification/grantVerifications.json
new file mode 100644
index 00000000..2b51999c
--- /dev/null
+++ b/lexicons/tools/ozone/verification/grantVerifications.json
@@ -0,0 +1,92 @@
+{
+  "lexicon": 1,
+  "id": "tools.ozone.verification.grantVerifications",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Grant verifications to multiple subjects. Allows batch processing of up to 100 verifications at once.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["verifications"],
+          "properties": {
+            "verifications": {
+              "type": "array",
+              "description": "Array of verification requests to process",
+              "maxLength": 100,
+              "items": {
+                "type": "ref",
+                "ref": "#verificationInput"
+              }
+            }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["verifications", "failedVerifications"],
+          "properties": {
+            "verifications": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "tools.ozone.verification.defs#verificationView"
+              }
+            },
+            "failedVerifications": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "#grantError"
+              }
+            }
+          }
+        }
+      }
+    },
+    "verificationInput": {
+      "type": "object",
+      "required": ["subject", "handle", "displayName"],
+      "properties": {
+        "subject": {
+          "type": "string",
+          "description": "The did of the subject being verified",
+          "format": "did"
+        },
+        "handle": {
+          "type": "string",
+          "description": "Handle of the subject the verification applies to at the moment of verifying.",
+          "format": "handle"
+        },
+        "displayName": {
+          "type": "string",
+          "description": "Display name of the subject the verification applies to at the moment of verifying."
+        },
+        "createdAt": {
+          "type": "string",
+          "format": "datetime",
+          "description": "Timestamp for verification record. Defaults to current time when not specified."
+        }
+      }
+    },
+    "grantError": {
+      "type": "object",
+      "description": "Error object for failed verifications.",
+      "required": ["error", "subject"],
+      "properties": {
+        "error": {
+          "type": "string",
+          "description": "Error message describing the reason for failure."
+        },
+        "subject": {
+          "type": "string",
+          "description": "The did of the subject being verified",
+          "format": "did"
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/tools/ozone/verification/listVerifications.json b/lexicons/tools/ozone/verification/listVerifications.json
new file mode 100644
index 00000000..33904eb1
--- /dev/null
+++ b/lexicons/tools/ozone/verification/listVerifications.json
@@ -0,0 +1,83 @@
+{
+  "lexicon": 1,
+  "id": "tools.ozone.verification.listVerifications",
+  "defs": {
+    "main": {
+      "type": "query",
+      "description": "List verifications",
+      "parameters": {
+        "type": "params",
+        "properties": {
+          "cursor": {
+            "type": "string",
+            "description": "Pagination cursor"
+          },
+          "limit": {
+            "type": "integer",
+            "description": "Maximum number of results to return",
+            "minimum": 1,
+            "maximum": 100,
+            "default": 50
+          },
+          "createdAfter": {
+            "type": "string",
+            "format": "datetime",
+            "description": "Filter to verifications created after this timestamp"
+          },
+          "createdBefore": {
+            "type": "string",
+            "format": "datetime",
+            "description": "Filter to verifications created before this timestamp"
+          },
+          "issuers": {
+            "type": "array",
+            "maxLength": 100,
+            "description": "Filter to verifications from specific issuers",
+            "items": {
+              "type": "string",
+              "format": "did"
+            }
+          },
+          "subjects": {
+            "type": "array",
+            "description": "Filter to specific verified DIDs",
+            "maxLength": 100,
+            "items": {
+              "type": "string",
+              "format": "did"
+            }
+          },
+          "sortDirection": {
+            "type": "string",
+            "description": "Sort direction for creation date",
+            "enum": ["asc", "desc"],
+            "default": "desc"
+          },
+          "isRevoked": {
+            "type": "boolean",
+            "description": "Filter to verifications that are revoked or not. By default, includes both."
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["verifications"],
+          "properties": {
+            "cursor": {
+              "type": "string"
+            },
+            "verifications": {
+              "type": "array",
+              "items": {
+                "type": "ref",
+                "ref": "tools.ozone.verification.defs#verificationView"
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lexicons/tools/ozone/verification/revokeVerifications.json b/lexicons/tools/ozone/verification/revokeVerifications.json
new file mode 100644
index 00000000..f3759d44
--- /dev/null
+++ b/lexicons/tools/ozone/verification/revokeVerifications.json
@@ -0,0 +1,75 @@
+{
+  "lexicon": 1,
+  "id": "tools.ozone.verification.revokeVerifications",
+  "defs": {
+    "main": {
+      "type": "procedure",
+      "description": "Revoke previously granted verifications in batches of up to 100.",
+      "input": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["uris"],
+          "properties": {
+            "uris": {
+              "type": "array",
+              "description": "Array of verification record uris to revoke",
+              "maxLength": 100,
+              "items": {
+                "type": "string",
+                "description": "The AT-URI of the verification record to revoke.",
+                "format": "at-uri"
+              }
+            },
+            "revokeReason": {
+              "type": "string",
+              "description": "Reason for revoking the verification. This is optional and can be omitted if not needed.",
+              "maxLength": 1000
+            }
+          }
+        }
+      },
+      "output": {
+        "encoding": "application/json",
+        "schema": {
+          "type": "object",
+          "required": ["revokedVerifications", "failedRevocations"],
+          "properties": {
+            "revokedVerifications": {
+              "type": "array",
+              "description": "List of verification uris successfully revoked",
+              "items": {
+                "type": "string",
+                "format": "at-uri"
+              }
+            },
+            "failedRevocations": {
+              "type": "array",
+              "description": "List of verification uris that couldn't be revoked, including failure reasons",
+              "items": {
+                "type": "ref",
+                "ref": "#revokeError"
+              }
+            }
+          }
+        }
+      }
+    },
+    "revokeError": {
+      "type": "object",
+      "description": "Error object for failed revocations",
+      "required": ["uri", "error"],
+      "properties": {
+        "uri": {
+          "type": "string",
+          "description": "The AT-URI of the verification record that failed to revoke.",
+          "format": "at-uri"
+        },
+        "error": {
+          "type": "string",
+          "description": "Description of the error that occurred during revocation."
+        }
+      }
+    }
+  }
+}



Test patch:
diff --git a/atproto/lexicon/interop_language_test.go b/atproto/lexicon/interop_language_test.go
index fa9f347e..43a4396e 100644
--- a/atproto/lexicon/interop_language_test.go
+++ b/atproto/lexicon/interop_language_test.go
@@ -2,6 +2,7 @@ package lexicon
 
 import (
 	"encoding/json"
+	"fmt"
 	"io"
 	"os"
 	"testing"
@@ -91,5 +92,14 @@ func testLexiconFixtureInvalid(t *testing.T, fixture LexiconFixture) {
 
 	var schema SchemaFile
 	err := json.Unmarshal(fixture.Lexicon, &schema)
+	if err == nil {
+		err = schema.FinishParse()
+	}
+	if err == nil {
+		err = schema.CheckSchema()
+	}
+	if err == nil {
+		fmt.Println(fixture.Name)
+	}
 	assert.Error(err)
 }
diff --git a/atproto/lexicon/mimetype_test.go b/atproto/lexicon/mimetype_test.go
index db2be81d..626ccc67 100644
--- a/atproto/lexicon/mimetype_test.go
+++ b/atproto/lexicon/mimetype_test.go
@@ -11,6 +11,7 @@ func TestAcceptableMimeType(t *testing.T) {
 
 	assert.True(acceptableMimeType("image/*", "image/png"))
 	assert.True(acceptableMimeType("text/plain", "text/plain"))
+	assert.True(acceptableMimeType("*/*", "text/plain"))
 
 	assert.False(acceptableMimeType("image/*", "text/plain"))
 	assert.False(acceptableMimeType("text/plain", "image/png"))
diff --git a/atproto/lexicon/testdata/catalog/permission-set.json b/atproto/lexicon/testdata/catalog/permission-set.json
index c0d93be8..a2079ab9 100644
--- a/atproto/lexicon/testdata/catalog/permission-set.json
+++ b/atproto/lexicon/testdata/catalog/permission-set.json
@@ -14,14 +14,6 @@
         "fr-FR": "Créer des rapports de modération"
       },
       "permissions": [
-        {
-          "type": "permission",
-          "resource": "blob",
-          "accept": [
-            "image/*",
-            "video/*"
-          ]
-        },
         {
           "type": "permission",
           "resource": "repo",
@@ -60,34 +52,26 @@
         {
           "type": "permission",
           "resource": "rpc",
-          "aud": "did:web:example.com#foo",
           "lxm": [
             "com.example.calendar.listEvents"
-          ]
+          ],
+          "aud": "*"
         },
         {
           "type": "permission",
           "resource": "rpc",
-          "aud": "did:web:example.com#bar",
           "lxm": [
             "*"
-          ]
-        },
-        {
-          "type": "permission",
-          "resource": "rpc",
-          "inheritAud": true,
-          "lxm": [
-            "com.example.calendar.listEvents"
-          ]
+          ],
+          "inheritAud": true
         },
         {
           "type": "permission",
           "resource": "rpc",
-          "aud": "*",
           "lxm": [
             "com.example.calendar.listEvents"
-          ]
+          ],
+          "inheritAud": true
         }
       ]
     }
diff --git a/atproto/lexicon/testdata/catalog/procedure.json b/atproto/lexicon/testdata/catalog/procedure.json
index 4ac58d22..4829a3ab 100644
--- a/atproto/lexicon/testdata/catalog/procedure.json
+++ b/atproto/lexicon/testdata/catalog/procedure.json
@@ -16,7 +16,7 @@
             "type": "integer",
             "description": "field of type integer"
           },
-          "string": {
+          "stringField": {
             "type": "string",
             "description": "field of type string"
           }
diff --git a/atproto/lexicon/testdata/catalog/query.json b/atproto/lexicon/testdata/catalog/query.json
index 3c659b44..c355d0ea 100644
--- a/atproto/lexicon/testdata/catalog/query.json
+++ b/atproto/lexicon/testdata/catalog/query.json
@@ -10,7 +10,7 @@
         "type": "params",
         "description": "a params type",
         "required": [
-          "string"
+          "stringField"
         ],
         "properties": {
           "boolean": {
@@ -21,7 +21,7 @@
             "type": "integer",
             "description": "field of type integer"
           },
-          "string": {
+          "stringField": {
             "type": "string",
             "description": "field of type string"
           },
@@ -30,10 +30,6 @@
             "format": "handle",
             "description": "field of type string, format handle"
           },
-          "unknown": {
-            "type": "unknown",
-            "description": "field of type unknown"
-          },
           "array": {
             "type": "array",
             "description": "field of type array",
diff --git a/atproto/lexicon/testdata/catalog/record.json b/atproto/lexicon/testdata/catalog/record.json
index 42bc4e36..efe52c0c 100644
--- a/atproto/lexicon/testdata/catalog/record.json
+++ b/atproto/lexicon/testdata/catalog/record.json
@@ -8,6 +8,7 @@
       "key": "literal:demo",
       "description": "a record type with many field",
       "record": {
+        "type": "object",
         "required": [
           "integer"
         ],
@@ -15,10 +16,6 @@
           "nullableString"
         ],
         "properties": {
-          "null": {
-            "type": "null",
-            "description": "field of type null"
-          },
           "boolean": {
             "type": "boolean",
             "description": "field of type boolean"
@@ -60,7 +57,7 @@
           },
           "object": {
             "type": "object",
-            "description": "field of type null",
+            "description": "field of type object",
             "properties": {
               "a": {
                 "type": "integer"
@@ -73,7 +70,7 @@
           "ref": {
             "type": "ref",
             "description": "field of type ref",
-            "ref": "example.lexicon.record#demoToken"
+            "ref": "example.lexicon.record#demoObject"
           },
           "union": {
             "type": "union",
diff --git a/atproto/lexicon/testdata/lexicon-invalid.json b/atproto/lexicon/testdata/lexicon-invalid.json
index 923779af..4c2786a4 100644
--- a/atproto/lexicon/testdata/lexicon-invalid.json
+++ b/atproto/lexicon/testdata/lexicon-invalid.json
@@ -22,5 +22,66 @@
         }
       }
     }
+  },
+  {
+    "name": "defined unknown",
+    "lexicon": {
+      "lexicon": 1,
+      "id": "example.lexicon",
+      "defs": {
+        "demo": {
+          "type": "unknown"
+        }
+      }
+    }
+  },
+  {
+    "name": "defined ref",
+    "lexicon": {
+      "lexicon": 1,
+      "id": "example.lexicon",
+      "defs": {
+        "demo": {
+          "type": "ref",
+          "ref": "com.atproto.repo.strongRef"
+        }
+      }
+    }
+  },
+  {
+    "name": "non-main primary",
+    "lexicon": {
+      "lexicon": 1,
+      "id": "example.lexicon",
+      "defs": {
+        "demo": {
+          "type": "record",
+          "key": "any",
+          "record": {
+            "type": "object"
+          }
+        }
+      }
+    }
+  },
+  {
+    "name": "record missing type object",
+    "lexicon": {
+      "lexicon": 1,
+      "id": "example.lexicon",
+      "defs": {
+        "main": {
+          "type": "record",
+          "key": "any",
+          "record": {
+            "properties": {
+              "b": {
+                "type": "boolean"
+              }
+            }
+          }
+        }
+      }
+    }
   }
 ]
diff --git a/atproto/lexicon/testdata/lexicon-valid.json b/atproto/lexicon/testdata/lexicon-valid.json
index e497aab2..16a8fbc8 100644
--- a/atproto/lexicon/testdata/lexicon-valid.json
+++ b/atproto/lexicon/testdata/lexicon-valid.json
@@ -10,5 +10,67 @@
         }
       }
     }
+  },
+  {
+    "name": "minimal record",
+    "lexicon": {
+      "lexicon": 1,
+      "id": "example.lexicon.record",
+      "defs": {
+        "main": {
+          "type": "record",
+          "key": "any",
+          "record": {
+            "type": "object",
+            "properties": {}
+          }
+        }
+      }
+    }
+  },
+  {
+    "name": "basic permission-set",
+    "lexicon": {
+      "lexicon": 1,
+      "id": "example.lexicon.perms",
+      "defs": {
+        "main": {
+          "type": "permission-set",
+          "title": "test case",
+          "permissions": [
+            {
+              "type": "permission",
+              "resource": "repo",
+              "collection": [
+                "com.example.calendar.event"
+              ],
+              "action": [
+                "delete",
+                "create"
+              ]
+            },
+            {
+              "type": "permission",
+              "resource": "repo",
+              "collection": [
+                "com.example.calendar.rsvp"
+              ]
+            },
+            {
+              "type": "permission",
+              "resource": "rpc",
+              "lxm": ["example.lexicon.endpoint"],
+              "aud": "*"
+            },
+            {
+              "type": "permission",
+              "resource": "rpc",
+              "lxm": ["example.lexicon.endpointTwo"],
+              "inheritAud": true
+            }
+          ]
+        }
+      }
+    }
   }
 ]
diff --git a/atproto/lexicon/testdata/record-data-invalid.json b/atproto/lexicon/testdata/record-data-invalid.json
index 16008125..17af3e7d 100644
--- a/atproto/lexicon/testdata/record-data-invalid.json
+++ b/atproto/lexicon/testdata/record-data-invalid.json
@@ -6,15 +6,6 @@
       "$type": "example.lexicon.record"
     }
   },
-  {
-    "name": "invalid null field",
-    "rkey": "demo",
-    "data": {
-      "$type": "example.lexicon.record",
-      "integer": 1,
-      "null": true
-    }
-  },
   {
     "name": "invalid boolean field",
     "rkey": "demo",
diff --git a/atproto/lexicon/testdata/record-data-valid.json b/atproto/lexicon/testdata/record-data-valid.json
index 4becd8b7..3eb0e023 100644
--- a/atproto/lexicon/testdata/record-data-valid.json
+++ b/atproto/lexicon/testdata/record-data-valid.json
@@ -12,7 +12,6 @@
     "rkey": "demo",
     "data": {
       "$type": "example.lexicon.record",
-      "null": null,
       "boolean": true,
       "integer": 3,
       "string": "blah",
@@ -44,7 +43,10 @@
         "a": 1,
         "b": 2
       },
-      "ref": "example.lexicon.record#demoToken",
+      "ref": {
+        "a": 1,
+        "b": 2
+      },
       "union": {
         "$type": "example.lexicon.record#demoObject",
         "a": 1,
diff --git a/atproto/lexicon/lexicon_test.go b/atproto/lexicon/validation_test.go
similarity index 100%
rename from atproto/lexicon/lexicon_test.go
rename to atproto/lexicon/validation_test.go
diff --git a/cmd/relay/stream/persist/diskpersist/diskpersist_test.go b/cmd/relay/stream/persist/diskpersist/diskpersist_test.go
new file mode 100644
index 00000000..69573e49
--- /dev/null
+++ b/cmd/relay/stream/persist/diskpersist/diskpersist_test.go
@@ -0,0 +1,76 @@
+package diskpersist
+
+import (
+	"path/filepath"
+	"testing"
+	"time"
+
+	"github.com/bluesky-social/indigo/api/atproto"
+	"github.com/bluesky-social/indigo/cmd/relay/stream"
+	lexutil "github.com/bluesky-social/indigo/lex/util"
+	"github.com/bluesky-social/indigo/util"
+	"github.com/ipfs/go-cid"
+	mh "github.com/multiformats/go-multihash"
+	"github.com/stretchr/testify/require"
+	"gorm.io/driver/sqlite"
+	"gorm.io/gorm"
+)
+
+func testCid() cid.Cid {
+	buf := make([]byte, 32)
+	c, err := cid.NewPrefixV1(cid.Raw, mh.SHA2_256).Sum(buf)
+	if err != nil {
+		panic(err)
+	}
+	return c
+}
+
+func TestInitialSequenceNumber(t *testing.T) {
+	require := require.New(t)
+
+	dir := t.TempDir()
+
+	db, err := gorm.Open(sqlite.Open(filepath.Join(dir, "relay.sqlite")))
+	require.NoError(err)
+
+	// Open the persistence, the current sequence should be 1 (the default):
+	persist, err := NewDiskPersistence(dir, "", db, DefaultDiskPersistOptions())
+	require.NoError(err)
+	require.Equal(int64(1), persist.curSeq)
+
+	// Shutdown the persister:
+	err = persist.Shutdown(t.Context())
+	require.NoError(err)
+
+	// Reopen the persistence, the current sequence should still be 1:
+	persist, err = NewDiskPersistence(dir, "", db, DefaultDiskPersistOptions())
+	require.NoError(err)
+	require.Equal(int64(1), persist.curSeq)
+
+	// Fake the DID to UID mapping:
+	did := "did:example:123"
+	persist.didCache.Add(did, 123)
+
+	// Insert a dummy event:
+	event := &stream.XRPCStreamEvent{
+		RepoCommit: &atproto.SyncSubscribeRepos_Commit{
+			Repo:   did,
+			Commit: lexutil.LexLink(testCid()),
+			Time:   time.Now().Format(util.ISO8601),
+		},
+	}
+	err = persist.Persist(t.Context(), event)
+	require.NoError(err)
+
+	// Sequence number should now be 2:
+	require.Equal(int64(2), persist.curSeq)
+
+	// Shutdown the persister:
+	err = persist.Shutdown(t.Context())
+	require.NoError(err)
+
+	// Reopen the persistence, the current sequence should still be 2:
+	persist, err = NewDiskPersistence(dir, "", db, DefaultDiskPersistOptions())
+	require.NoError(err)
+	require.Equal(int64(2), persist.curSeq)
+}
diff --git a/lex/lexgen/util_test.go b/lex/lexgen/util_test.go
new file mode 100644
index 00000000..fba7f4ae
--- /dev/null
+++ b/lex/lexgen/util_test.go
@@ -0,0 +1,25 @@
+package lexgen
+
+import (
+	"testing"
+
+	"github.com/bluesky-social/indigo/atproto/syntax"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestNSIDNames(t *testing.T) {
+	assert := assert.New(t)
+
+	testVectors := [][]string{
+		{"app.bsky.feed.post", "appbsky", "FeedPost"},
+		{"com.atproto.admin.deleteAccount", "comatproto", "AdminDeleteAccount"},
+		{"uk.ac.school.lab.COOL.project", "ukacschool", "LabCoolProject"},
+	}
+
+	for _, vec := range testVectors {
+		nsid := syntax.NSID(vec[0])
+		assert.Equal(vec[1], nsidPkgName(nsid))
+		assert.Equal(vec[2], nsidBaseName(nsid))
+	}
+}
diff --git a/lex/lexlint/lint_test.go b/lex/lexlint/lint_test.go
new file mode 100644
index 00000000..8100a601
--- /dev/null
+++ b/lex/lexlint/lint_test.go
@@ -0,0 +1,54 @@
+package lexlint
+
+import (
+	"encoding/json"
+	"io"
+	"os"
+	"sort"
+	"testing"
+
+	"github.com/bluesky-social/indigo/atproto/lexicon"
+
+	"github.com/stretchr/testify/assert"
+)
+
+type LintFixture struct {
+	Name   string             `json:"name"`
+	Schema lexicon.SchemaFile `json:"schema"`
+	Issues []string           `json:"issues"`
+}
+
+func TestLexLintFixtures(t *testing.T) {
+	assert := assert.New(t)
+
+	f, err := os.Open("testdata/lint-examples.json")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer func() { _ = f.Close() }()
+
+	jsonBytes, err := io.ReadAll(f)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	var fixtures []LintFixture
+	if err := json.Unmarshal(jsonBytes, &fixtures); err != nil {
+		t.Fatal(err)
+	}
+
+	for _, f := range fixtures {
+		assert.NoError(f.Schema.FinishParse())
+
+		issues := LintSchemaFile(&f.Schema)
+
+		found := []string{}
+		for _, iss := range issues {
+			found = append(found, iss.LintName)
+		}
+
+		sort.Strings(found)
+		sort.Strings(f.Issues)
+		assert.Equal(f.Issues, found, f.Name)
+	}
+}
diff --git a/lex/lexlint/syntax_test.go b/lex/lexlint/syntax_test.go
new file mode 100644
index 00000000..dd3a9967
--- /dev/null
+++ b/lex/lexlint/syntax_test.go
@@ -0,0 +1,36 @@
+package lexlint
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestCheckSchemaName(t *testing.T) {
+	assert := assert.New(t)
+
+	goodNames := []string{
+		"blahFunc",
+		"blahFuncV2",
+	}
+
+	badNames := []string{
+		"",
+		" ",
+		"blah Func",
+		"blah_Func",
+		"blah-Func",
+		" blahFunc",
+		"one.two",
+		".",
+		"2blahFunc",
+	}
+
+	for _, name := range goodNames {
+		assert.NoError(CheckSchemaName(name))
+	}
+
+	for _, name := range badNames {
+		assert.Error(CheckSchemaName(name))
+	}
+}
diff --git a/lex/lexlint/testdata/lint-examples.json b/lex/lexlint/testdata/lint-examples.json
new file mode 100644
index 00000000..300037b5
--- /dev/null
+++ b/lex/lexlint/testdata/lint-examples.json
@@ -0,0 +1,76 @@
+[
+  {
+    "name": "minimal, no lint problems",
+    "schema": {
+      "lexicon": 1,
+      "id": "example.lexicon.record",
+      "defs": {
+        "main": {
+          "type": "record",
+          "description": "minimal description",
+          "key": "any",
+          "record": {
+            "type": "object",
+            "properties": {}
+          }
+        }
+      }
+    },
+    "issues": []
+  },
+  {
+    "name": "bad field name",
+    "schema": {
+      "lexicon": 1,
+      "id": "example.lexicon.record",
+      "defs": {
+        "main": {
+          "type": "record",
+          "description": "minimal description",
+          "key": "any",
+          "record": {
+            "type": "object",
+            "properties": {
+              "one.two": {
+                "type": "string",
+                "maxLength": 20
+              }
+            }
+          }
+        }
+      }
+    },
+    "issues": ["field-name-syntax"]
+  },
+  {
+    "name": "local ref in string known values",
+    "schema": {
+      "lexicon": 1,
+      "id": "example.lexicon.record",
+      "defs": {
+        "main": {
+          "type": "record",
+          "description": "minimal description",
+          "key": "any",
+          "record": {
+            "type": "object",
+            "properties": {
+              "strField": {
+                "type": "string",
+                "maxLength": 100,
+                "knownValues": [
+                  "#green"
+                ]
+              }
+            }
+          }
+        },
+        "green": {
+          "type": "token",
+          "description": "asdf"
+        }
+      }
+    },
+    "issues": ["known-string-local-ref"]
+  }
+]