Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .agents/skills/hypercerts-api/references/endpoint-map.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ This task index lists the public query NSIDs in the bundled [OpenAPI contract](o
- **Browse or filter without free text:** choose `list...` in the relevant domain.
- **Text discovery:** use `search...` only where listed below. A search query is not interchangeable with a list filter, and search semantics vary by endpoint.
- **Relationship checks:** use `get...Follow` for a single pair, or `getFollows` / `getEntityFollows` to check repeated target values for one actor. List queries answer who follows whom or which entities an actor follows.
- **Follower counts:** use `getFollowerCounts` for counts across many accounts and entities in one call; use a follower list's `totalCount` only when you also need that subject's followers.
- **Feed reactions:** use `getLikes` or `getReposts` for distinct actors reacting to one subject URI, `getActorLikes` / `getActorReposts` for one actor's raw representative reactions, `getActorReactions` to check one actor across a bounded set of known subject URIs, or `getReactionCounts` for indexed like and repost counts across known subjects.
- **Comment counts:** use `org.hypercerts.context.getCommentCounts` for comment counts across known subjects. Comments are attachments, not feed reactions; list them with `listAttachments` filtered by `subjects` and `contentTypes=comment`.

Expand Down Expand Up @@ -70,11 +71,12 @@ Choose `listCollectionItems` when the caller needs the referenced item versions,

An `unanswered` award is a successful indexed award result, not a not-found response. See [the badge recipe](recipes.md#read-badge-award-response-status).

## Follow relationships (9)
## Follow relationships (10)

| User task | Query NSID | Practical choice |
|---|---|---|
| Check one actor-to-actor follow | `app.certified.graph.getFollow` | Supply actor and subject DIDs; the representative `follow` may be null when none is indexed. |
| Count followers for many accounts and entities | `app.certified.graph.getFollowerCounts` | Supply 1–100 repeated `subjects` values, mixing account DIDs and entity AT-URIs freely. Results preserve input order and duplicates; each count equals that subject's `listActorFollowers` or `listEntityFollowers` `totalCount`. |
| Check one actor against many subject DIDs | `app.certified.graph.getFollows` | Supply 1–100 repeated `subjects` values; each `{subject, follow}` result preserves input order and duplicates. |
| List an actor's followers | `app.certified.graph.listActorFollowers` | `actor` is the followed actor DID. |
| List actors an actor follows | `app.certified.graph.listActorFollowing` | `actor` is the follower DID. |
Expand Down
79 changes: 79 additions & 0 deletions .agents/skills/hypercerts-api/references/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -1466,6 +1466,56 @@
]
}
},
"/xrpc/app.certified.graph.getFollowerCounts": {
"get": {
"operationId": "app_certified_graph_getFollowerCounts",
"tags": ["app.certified.graph"],
"description": "Returns current indexed follower counts for a mixed list of accounts and entities. No authentication is required. A DID subject counts distinct publishers of app.certified.graph.follow records naming that DID; an AT-URI subject counts distinct publishers of app.certified.graph.entityFollow records naming that URI. Counts match listActorFollowers and listEntityFollowers totalCount, and do not require the subject to be indexed.",
"parameters": [
{
"name": "subjects",
"in": "query",
"required": true,
"description": "Pass 1–100 account DIDs or full DID-authority entity AT-URIs, mixed freely, as repeated unbracketed subjects query parameters. The encoded GET request-target must not exceed 7,000 bytes.",
"schema": {
"type": "array",
"minItems": 1,
"maxItems": 100,
"items": {
"type": "string",
"format": "uri",
"maxLength": 8192
}
},
"style": "form",
"explode": true
}
],
"responses": {
"200": {
"description": "Successful response.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/app.certified.graph.getFollowerCounts.output"
}
}
}
}
},
"x-lexicon-id": "app.certified.graph.getFollowerCounts",
"x-lexicon-errors": [
{
"name": "InvalidRequest",
"description": "The subjects list is empty, has more than 100 entries, exceeds the request-target limit, or contains a value that is neither a valid DID nor a full DID-authority AT-URI; correct the list and retry."
},
{
"name": "FollowerCountQueryFailed",
"description": "The indexed follower count query failed; retry the request."
}
]
}
},
"/xrpc/app.certified.graph.getFollows": {
"get": {
"operationId": "app_certified_graph_getFollows",
Expand Down Expand Up @@ -4834,6 +4884,35 @@
}
}
},
"app.certified.graph.getFollowerCounts.subjectFollowerCount": {
"type": "object",
"required": ["subject", "followerCount"],
"properties": {
"subject": {
"type": "string",
"description": "Subject DID or AT-URI exactly as requested by the caller.",
"format": "uri"
},
"followerCount": {
"type": "integer",
"description": "Number of distinct indexed publisher DIDs with a current follow of this subject; zero when none are indexed.",
"minimum": 0
}
}
},
"app.certified.graph.getFollowerCounts.output": {
"type": "object",
"required": ["counts"],
"properties": {
"counts": {
"type": "array",
"description": "One entry for each requested subject, in request order. Repeated inputs produce repeated entries. Counts reflect current indexed state and may change between requests.",
"items": {
"$ref": "#/components/schemas/app.certified.graph.getFollowerCounts.subjectFollowerCount"
}
}
}
},
"app.certified.graph.getFollows.followResult": {
"type": "object",
"required": ["subject", "follow"],
Expand Down
2 changes: 2 additions & 0 deletions .agents/skills/hypercerts-api/references/request-patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ A useful starting distinction: arrays often mean “any of these” for a filter

The batch relationship checks `app.certified.graph.getFollows` and `app.certified.graph.getEntityFollows` each take one `actor` and 1–100 repeated, unbracketed `subjects` or `entities` values. Keep duplicates if the caller supplied them: each output `results` entry echoes its target and preserves its position. The encoded GET request-target must stay within 7,000 bytes. A null `follow` means only that no indexed relationship was found; database failures are errors, not nulls.

`app.certified.graph.getFollowerCounts` takes no `actor`: append 1–100 account DIDs and full DID-authority entity AT-URIs under the same unbracketed `subjects` key, in any mix. A value starting with `did:` is counted as an account; any other value must be an entity AT-URI. The response has one `{subject, followerCount}` entry per input occurrence in the same order, including duplicates. The same 7,000-byte request-target limit applies, and a zero count means no indexed followers were found at query time.

### Batch an actor's reactions for known subjects

`app.certified.feed.getActorReactions` requires one actor DID and 1–100 full record AT-URIs with DID authorities, not handle authorities. Encode `subjects` as repeated, unbracketed query parameters; keep input order and duplicates if each occurrence matters:
Expand Down
6 changes: 6 additions & 0 deletions .changeset/batch-follower-counts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@hypercerts-org/happyview-modules': minor
'@hypercerts-org/api': minor
---

Add `app.certified.graph.getFollowerCounts`, which returns follower counts for up to 100 account DIDs and entity AT-URIs in one call.
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ The HTTP gate fails if it discovers no suites, executes no `node:test` cases, or
- Context attachment and evaluation retrieval with publisher/evaluator sidecar hydration, list filters, pagination across tied `createdAt` values, and named runtime errors. Batched comment counts cover per-record counting, CID-independent subject matching, excluded non-comment attachments, and agreement with `listAttachments`.
- Activity retrieval with contributor-sidecar hydration, author/organization/contributor/URI filters, tied timestamp pagination, and literal wildcard search.
- Collection retrieval with CBOR-derived CIDs, location/tag projections, author, organization, item and tag filters, title/shortDescription search, `createdAt`/URI pagination ties, and source-order item pagination with exact-version resolution.
- Graph actor/entity lookups and lists, tied-key pagination, nullable profile/organization sidecars, entity target resolution, entity-follower totals across pages and empty pages, and the global recent-follows `before` filter. The `getFollows` and `getEntityFollows` batch queries also cover repeated target order and duplicates, explicit target keys, nullable raw views, and invalid-entry rejection.
- Graph actor/entity lookups and lists, tied-key pagination, nullable profile/organization sidecars, entity target resolution, entity-follower totals across pages and empty pages, and the global recent-follows `before` filter. The `getFollows` and `getEntityFollows` batch queries also cover repeated target order and duplicates, explicit target keys, nullable raw views, and invalid-entry rejection. The `getFollowerCounts` batch covers mixed DID and entity subjects, input order and duplicates, deduplicated publishers, zero counts, agreement with each list endpoint's `totalCount`, and invalid-subject rejection.
- Feed-reaction subject and actor queries, earliest duplicate selection, URI-only subject matching across CIDs, distinct-actor totals, tied cursor pagination, page-only sidecar hydration, raw `via`/signature preservation, and actor queries without hydration.
- Contribution exact-record retrieval, repeated publisher filters, tied ascending/descending cursor pagination, nullable publisher sidecars, and named errors.
- Badge, badge-query, acknowledgement, contribution, and rights HTTP fixtures with CBOR-derived record CIDs; contribution DIDs are distinct from baseline fixture identities.
Expand Down
12 changes: 9 additions & 3 deletions docs/coverage.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
},
"runtimeValidation": "not-assessed",
"deploymentValidation": "not-assessed",
"endpointCount": 64,
"endpointCount": 65,
"methods": {
"GET": 64
"GET": 65
},
"inclusion": {
"manifest-registered": 64
"manifest-registered": 65
},
"unresolvedReferences": [],
"endpoints": [
Expand Down Expand Up @@ -158,6 +158,12 @@
"inclusion": "manifest-registered",
"module": "modules/actor-follow/manifest.json"
},
{
"id": "app.certified.graph.getFollowerCounts",
"method": "GET",
"inclusion": "manifest-registered",
"module": "modules/follower-counts/manifest.json"
},
{
"id": "app.certified.graph.getFollows",
"method": "GET",
Expand Down
90 changes: 90 additions & 0 deletions docs/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -1894,6 +1894,62 @@
]
}
},
"/xrpc/app.certified.graph.getFollowerCounts": {
"get": {
"operationId": "app_certified_graph_getFollowerCounts",
"tags": [
"app.certified.graph"
],
"description": "Returns current indexed follower counts for a mixed list of accounts and entities. No authentication is required. A DID subject counts distinct publishers of app.certified.graph.follow records naming that DID; an AT-URI subject counts distinct publishers of app.certified.graph.entityFollow records naming that URI. Counts match listActorFollowers and listEntityFollowers totalCount, and do not require the subject to be indexed.",
"parameters": [
{
"name": "subjects",
"in": "query",
"required": true,
"description": "Pass 1–100 account DIDs or full DID-authority entity AT-URIs, mixed freely, as repeated unbracketed subjects query parameters. The encoded GET request-target must not exceed 7,000 bytes.",
"schema": {
"type": "array",
"minItems": 1,
"maxItems": 100,
"items": {
"type": "string",
"format": "uri",
"maxLength": 8192
}
},
"style": "form",
"explode": true
}
],
"responses": {
"200": {
"description": "Successful response.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/app.certified.graph.getFollowerCounts.output"
}
}
}
}
},
"x-lexicon-id": "app.certified.graph.getFollowerCounts",
"x-hypercerts-coverage": "manifest-registered",
"x-hypercerts-source": {
"module": "modules/follower-counts/manifest.json"
},
"x-lexicon-errors": [
{
"name": "InvalidRequest",
"description": "The subjects list is empty, has more than 100 entries, exceeds the request-target limit, or contains a value that is neither a valid DID nor a full DID-authority AT-URI; correct the list and retry."
},
{
"name": "FollowerCountQueryFailed",
"description": "The indexed follower count query failed; retry the request."
}
]
}
},
"/xrpc/app.certified.graph.getFollows": {
"get": {
"operationId": "app_certified_graph_getFollows",
Expand Down Expand Up @@ -6439,6 +6495,40 @@
}
}
},
"app.certified.graph.getFollowerCounts.subjectFollowerCount": {
"type": "object",
"required": [
"subject",
"followerCount"
],
"properties": {
"subject": {
"type": "string",
"description": "Subject DID or AT-URI exactly as requested by the caller.",
"format": "uri"
},
"followerCount": {
"type": "integer",
"description": "Number of distinct indexed publisher DIDs with a current follow of this subject; zero when none are indexed.",
"minimum": 0
}
}
},
"app.certified.graph.getFollowerCounts.output": {
"type": "object",
"required": [
"counts"
],
"properties": {
"counts": {
"type": "array",
"description": "One entry for each requested subject, in request order. Repeated inputs produce repeated entries. Counts reflect current indexed state and may change between requests.",
"items": {
"$ref": "#/components/schemas/app.certified.graph.getFollowerCounts.subjectFollowerCount"
}
}
}
},
"app.certified.graph.getFollows.followResult": {
"type": "object",
"required": [
Expand Down
18 changes: 18 additions & 0 deletions docs/sources/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,15 @@
"path": "../../lexicons/app.certified.graph.getFollow.json"
}
},
{
"id": "app.certified.graph.getFollowerCounts",
"type": "query",
"module": "modules/follower-counts/manifest.json",
"file": "sources/lexicons/app.certified.graph.getFollowerCounts.json",
"source": {
"path": "../../lexicons/app.certified.graph.getFollowerCounts.json"
}
},
{
"id": "app.certified.graph.getFollows",
"type": "query",
Expand Down Expand Up @@ -887,6 +896,15 @@
"path": "../../lexicons/app.certified.graph.getFollow.json"
}
},
{
"id": "app.certified.graph.getFollowerCounts",
"file": "sources/lexicons/app.certified.graph.getFollowerCounts.json",
"kind": "endpoint",
"source": {
"module": "modules/follower-counts/manifest.json",
"path": "../../lexicons/app.certified.graph.getFollowerCounts.json"
}
},
{
"id": "app.certified.graph.getFollows",
"file": "sources/lexicons/app.certified.graph.getFollows.json",
Expand Down
81 changes: 81 additions & 0 deletions docs/sources/lexicons/app.certified.graph.getFollowerCounts.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
{
"lexicon": 1,
"id": "app.certified.graph.getFollowerCounts",
"defs": {
"main": {
"type": "query",
"description": "Returns current indexed follower counts for a mixed list of accounts and entities. No authentication is required. A DID subject counts distinct publishers of app.certified.graph.follow records naming that DID; an AT-URI subject counts distinct publishers of app.certified.graph.entityFollow records naming that URI. Counts match listActorFollowers and listEntityFollowers totalCount, and do not require the subject to be indexed.",
"parameters": {
"type": "params",
"required": [
"subjects"
],
"properties": {
"subjects": {
"type": "array",
"minLength": 1,
"maxLength": 100,
"description": "Pass 1–100 account DIDs or full DID-authority entity AT-URIs, mixed freely, as repeated unbracketed subjects query parameters. The encoded GET request-target must not exceed 7,000 bytes.",
"items": {
"type": "string",
"format": "uri",
"maxLength": 8192
}
}
}
},
"output": {
"encoding": "application/json",
"schema": {
"type": "ref",
"ref": "#output"
}
},
"errors": [
{
"name": "InvalidRequest",
"description": "The subjects list is empty, has more than 100 entries, exceeds the request-target limit, or contains a value that is neither a valid DID nor a full DID-authority AT-URI; correct the list and retry."
},
{
"name": "FollowerCountQueryFailed",
"description": "The indexed follower count query failed; retry the request."
}
]
},
"subjectFollowerCount": {
"type": "object",
"required": [
"subject",
"followerCount"
],
"properties": {
"subject": {
"type": "string",
"format": "uri",
"description": "Subject DID or AT-URI exactly as requested by the caller."
},
"followerCount": {
"type": "integer",
"minimum": 0,
"description": "Number of distinct indexed publisher DIDs with a current follow of this subject; zero when none are indexed."
}
}
},
"output": {
"type": "object",
"required": [
"counts"
],
"properties": {
"counts": {
"type": "array",
"description": "One entry for each requested subject, in request order. Repeated inputs produce repeated entries. Counts reflect current indexed state and may change between requests.",
"items": {
"type": "ref",
"ref": "#subjectFollowerCount"
}
}
}
}
}
}
Loading
Loading