Skip to content
Open
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
2 changes: 1 addition & 1 deletion .agents/skills/hypercerts-api/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Read the relevant reference, then inspect the exact operation in the [bundled Op

## Bundled API contract

- [`references/openapi.json`](references/openapi.json) is a self-contained snapshot of all 59 query operations, including parameters, response schemas, declared XRPC errors, and their referenced schema definitions. The included contract makes the skill self-contained when installed separately.
- [`references/openapi.json`](references/openapi.json) is a self-contained snapshot of all 60 query operations, including parameters, response schemas, declared XRPC errors, and their referenced schema definitions. The included contract makes the skill self-contained when installed separately.
- To inspect an exact contract, find `/xrpc/<full NSID>` in the snapshot, read its operation, and follow any `#/components/schemas/...` references by schema name. Each operation and schema is stored on its own line for focused lookup.
- The snapshot describes declared behavior, not endpoint availability or data freshness on a particular host. Its OpenAPI `info.version` is not the deployed server's version.
- Use the API directly when a task needs live indexed results. Do not send extra diagnostic probes just to validate deployment; a deployment-specific availability check requires explicit approval for the target.
5 changes: 3 additions & 2 deletions .agents/skills/hypercerts-api/references/endpoint-map.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Endpoint map: choose by consumer task

This task index lists the 59 public query NSIDs in the bundled [OpenAPI contract](openapi.json). It helps choose an operation but does not replace its exact contract: inspect that operation and its referenced schemas for required parameters, types, defaults, limits, filter semantics, response fields, and declared errors. The static snapshot describes the contract, not availability or freshness on a selected deployment.
This task index lists the 60 public query NSIDs in the bundled [OpenAPI contract](openapi.json). It helps choose an operation but does not replace its exact contract: inspect that operation and its referenced schemas for required parameters, types, defaults, limits, filter semantics, response fields, and declared errors. The static snapshot describes the contract, not availability or freshness on a selected deployment.

## Fast selection rules

Expand Down Expand Up @@ -83,14 +83,15 @@ An `unanswered` award is a successful indexed award result, not a not-found resp

Do not confuse actor follows with entity follows; their identifiers and list outputs differ. Recent raw follows preserve duplicates and are not a snapshot; consult that endpoint's description before using cursors.

## Feed reactions (4)
## Feed reactions (5)

| User task | Query NSID | Practical choice |
|---|---|---|
| List actors who liked a record | `app.certified.feed.getLikes` | `subject` is a full record AT-URI; matching ignores CID. The response includes a total distinct-actor count, paginated actor sidecars, and each actor's raw representative like. |
| List actors who reposted a record | `app.certified.feed.getReposts` | `subject` is a full record AT-URI; matching ignores CID. The response includes a total distinct-actor count, paginated actor sidecars, and each actor's raw representative repost. |
| List an actor's likes | `app.certified.feed.getActorLikes` | `actor` is a repository-owner DID. Returns raw like records, one per subject URI; subjects need not be indexed. |
| List an actor's reposts | `app.certified.feed.getActorReposts` | `actor` is a repository-owner DID. Returns raw repost records, one per subject URI; subjects need not be indexed. |
| Get reaction counts for subjects | `app.certified.feed.getReactionCounts` | Pass 1–100 full subject AT-URIs as repeated, unbracketed `subjects` parameters. Results preserve input order and duplicates; counts are distinct indexed publisher DIDs matching subject URI regardless of CID or subject-record indexing. |

Reaction queries use the earliest record per author/subject URI, with URI ascending as the tie-breaker. They sort pages by the representative record's `createdAt` and URI; check each operation for exact cursor and response details.

Expand Down
3 changes: 3 additions & 0 deletions .agents/skills/hypercerts-api/references/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"/xrpc/app.certified.feed.getActorReposts": {"get":{"operationId":"app_certified_feed_getActorReposts","tags":["app.certified.feed"],"description":"Lists an actor's indexed reposts, one representative per subject URI regardless of subject CID. No authentication is required. Duplicates use the earliest createdAt, with URI ascending on ties. Returns raw repost records including subject, via, and signatures when present; subjects are not hydrated and need not be indexed.","parameters":[{"name":"actor","in":"query","required":true,"description":"Repository-owner DID of the actor whose reposts to list.","schema":{"type":"string","format":"did"}},{"name":"sortDirection","in":"query","required":false,"description":"Sort by representative repost createdAt and URI; defaults to desc.","schema":{"type":"string","enum":["asc","desc"]}},{"name":"limit","in":"query","required":false,"description":"Maximum relationships per page; defaults to 25.","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque next-page cursor bound to sortDirection; keep all other parameters unchanged between pages.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.feed.getActorReposts.output"}}}}},"x-lexicon-id":"app.certified.feed.getActorReposts","x-lexicon-errors":[{"name":"InvalidRequest","description":"The actor DID, sort direction, limit, cursor, or another query parameter is invalid."},{"name":"RepostQueryFailed","description":"The actor reposts query failed; retry the request."}]}},
"/xrpc/app.certified.feed.getLikes": {"get":{"operationId":"app_certified_feed_getLikes","tags":["app.certified.feed"],"description":"Lists distinct actors liking a subject record. No authentication is required. Matches subject.uri regardless of CID or whether the subject is indexed. Duplicate author and subject URI pairs use the earliest createdAt, with URI ascending on ties. Only the returned page is hydrated; missing sidecars are null and hydration failures return errors, not partial pages.","parameters":[{"name":"subject","in":"query","required":true,"description":"Full DID-authority record AT-URI; any target collection is accepted.","schema":{"type":"string","format":"at-uri"}},{"name":"sortDirection","in":"query","required":false,"description":"Sort by representative like createdAt and URI; defaults to desc.","schema":{"type":"string","enum":["asc","desc"]}},{"name":"limit","in":"query","required":false,"description":"Maximum relationships per page; defaults to 25.","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque next-page cursor bound to sortDirection; keep all other parameters unchanged between pages.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.feed.getLikes.output"}}}}},"x-lexicon-id":"app.certified.feed.getLikes","x-lexicon-errors":[{"name":"InvalidRequest","description":"The subject AT-URI, sort direction, limit, cursor, or another query parameter is invalid."},{"name":"LikeQueryFailed","description":"The like query, total count, or required actor hydration failed; retry the request."}]}},
"/xrpc/app.certified.feed.getReposts": {"get":{"operationId":"app_certified_feed_getReposts","tags":["app.certified.feed"],"description":"Lists distinct actors reposting a subject record. No authentication is required. Matches subject.uri regardless of CID or whether the subject is indexed. Duplicate author and subject URI pairs use the earliest createdAt, with URI ascending on ties. Only the returned page is hydrated; missing sidecars are null and hydration failures return errors, not partial pages.","parameters":[{"name":"subject","in":"query","required":true,"description":"Full DID-authority record AT-URI; any target collection is accepted.","schema":{"type":"string","format":"at-uri"}},{"name":"sortDirection","in":"query","required":false,"description":"Sort by representative repost createdAt and URI; defaults to desc.","schema":{"type":"string","enum":["asc","desc"]}},{"name":"limit","in":"query","required":false,"description":"Maximum relationships per page; defaults to 25.","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque next-page cursor bound to sortDirection; keep all other parameters unchanged between pages.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.feed.getReposts.output"}}}}},"x-lexicon-id":"app.certified.feed.getReposts","x-lexicon-errors":[{"name":"InvalidRequest","description":"The subject AT-URI, sort direction, limit, cursor, or another query parameter is invalid."},{"name":"RepostQueryFailed","description":"The repost query, total count, or required actor hydration failed; retry the request."}]}},
"/xrpc/app.certified.feed.getReactionCounts": {"get":{"operationId":"app_certified_feed_getReactionCounts","tags":["app.certified.feed"],"description":"Returns current indexed like and repost counts for a list of subject records. No authentication is required. Counts are distinct publisher DIDs and match subject.uri regardless of CID or whether the subject is indexed.","parameters":[{"name":"subjects","in":"query","required":true,"description":"Pass 1–100 full DID-authority subject record AT-URIs as repeated, unbracketed subjects query parameters.","schema":{"type":"array","minItems":1,"maxItems":100,"items":{"type":"string","format":"at-uri"}},"style":"form","explode":true}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.feed.getReactionCounts.output"}}}}},"x-lexicon-id":"app.certified.feed.getReactionCounts","x-lexicon-errors":[{"name":"InvalidRequest","description":"The subjects list is empty, has more than 100 entries, or contains an invalid subject AT-URI; correct the list and retry."},{"name":"ReactionCountQueryFailed","description":"The indexed like or repost count query failed; retry the request."}]}},
"/xrpc/app.certified.graph.getEntityFollow": {"get":{"operationId":"app_certified_graph_getEntityFollow","tags":["app.certified.graph"],"description":"Returns the representative entity-follow record for an actor and entity AT-URI, or null if none is indexed. No authentication is required; lookup failures return errors rather than null.","parameters":[{"name":"actor","in":"query","required":true,"description":"DID that published the entity-follow record.","schema":{"type":"string","format":"did"}},{"name":"entity","in":"query","required":true,"description":"Full AT-URI of the followed record using a DID authority; any target collection is accepted, and matching is by URI, not CID.","schema":{"type":"string","format":"at-uri"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.graph.getEntityFollow.output"}}}}},"x-lexicon-id":"app.certified.graph.getEntityFollow","x-lexicon-errors":[{"name":"InvalidRequest","description":"The actor DID, entity AT-URI, or another query parameter is invalid."}]}},
"/xrpc/app.certified.graph.getFollow": {"get":{"operationId":"app_certified_graph_getFollow","tags":["app.certified.graph"],"description":"Returns the representative follow record for an actor-to-actor relationship, or null if none is indexed. No authentication is required.","parameters":[{"name":"actor","in":"query","required":true,"description":"DID of the actor who published the follow record.","schema":{"type":"string","format":"did"}},{"name":"subject","in":"query","required":true,"description":"DID named as the follow record's subject.","schema":{"type":"string","format":"did"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.graph.getFollow.output"}}}}},"x-lexicon-id":"app.certified.graph.getFollow","x-lexicon-errors":[{"name":"InvalidRequest","description":"A DID or query parameter is invalid."}]}},
"/xrpc/app.certified.graph.listActorFollowers": {"get":{"operationId":"app_certified_graph_listActorFollowers","tags":["app.certified.graph"],"description":"Lists actors whose follow records name the requested DID as subject. No authentication is required; duplicate relationships are collapsed before pagination, and only the returned actors are hydrated.","parameters":[{"name":"actor","in":"query","required":true,"description":"DID whose incoming follows are listed.","schema":{"type":"string","format":"did"}},{"name":"sortDirection","in":"query","required":false,"description":"Sorts by representative follow createdAt and URI; defaults to desc.","schema":{"type":"string","enum":["asc","desc"]}},{"name":"limit","in":"query","required":false,"description":"Maximum relationships per page; defaults to 25.","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque next-page cursor bound to sortDirection; keep the other query parameters unchanged between pages.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.graph.listActorFollowers.output"}}}}},"x-lexicon-id":"app.certified.graph.listActorFollowers","x-lexicon-errors":[{"name":"InvalidRequest","description":"A DID or query parameter is invalid."}]}},
Expand Down Expand Up @@ -108,6 +109,8 @@
"app.certified.feed.getLikes.output": {"type":"object","required":["likeCount","likes"],"properties":{"likeCount":{"type":"integer","description":"Total distinct indexed author DIDs liking this subject URI, independent of page size. Zero when no likes are indexed; totals may change between requests.","minimum":0},"likes":{"type":"array","items":{"$ref":"#/components/schemas/app.certified.feed.getLikes.likeView"}},"cursor":{"type":"string","description":"Next-page cursor; omitted when pagination is complete."}}},
"app.certified.feed.getReposts.repostView": {"type":"object","description":"Reposter actor sidecars paired with the representative raw repost record. Missing profile and organization records are null.","required":["did","profile","organization","repost"],"properties":{"did":{"type":"string","format":"did"},"profile":{"anyOf":[{"$ref":"#/components/schemas/org.hypercerts.api.defs.profileView"},{"type":"null"}]},"organization":{"anyOf":[{"$ref":"#/components/schemas/org.hypercerts.api.defs.organizationView"},{"type":"null"}]},"repost":{"$ref":"#/components/schemas/app.certified.feed.getReposts.repostRecordView"}}},
"app.certified.feed.getReposts.output": {"type":"object","required":["repostCount","reposts"],"properties":{"repostCount":{"type":"integer","description":"Total distinct indexed author DIDs reposting this subject URI, independent of page size. Zero when no reposts are indexed; totals may change between requests.","minimum":0},"reposts":{"type":"array","items":{"$ref":"#/components/schemas/app.certified.feed.getReposts.repostView"}},"cursor":{"type":"string","description":"Next-page cursor; omitted when pagination is complete."}}},
"app.certified.feed.getReactionCounts.subjectReactionCount": {"type":"object","required":["subject","likeCount","repostCount"],"properties":{"subject":{"type":"string","description":"Subject AT-URI requested by the caller; matching ignores the subject CID.","format":"at-uri"},"likeCount":{"type":"integer","description":"Number of distinct indexed publisher DIDs with a current like whose subject.uri matches this URI; zero when no matching likes are indexed.","minimum":0},"repostCount":{"type":"integer","description":"Number of distinct indexed publisher DIDs with a current repost whose subject.uri matches this URI; zero when no matching reposts are indexed.","minimum":0}}},
"app.certified.feed.getReactionCounts.output": {"type":"object","required":["counts"],"properties":{"counts":{"type":"array","description":"One entry for each requested subject, in request order. Repeated input URIs produce repeated entries. Counts reflect current indexed state and may change between requests.","items":{"$ref":"#/components/schemas/app.certified.feed.getReactionCounts.subjectReactionCount"}}}},
"app.certified.graph.entityFollow.main": {"type":"record","description":"Record declaring a social 'follow' relationship with a non-account entity, such as a specific record. Account (DID) follows are out of scope and belong to app.certified.graph.follow. The subject is an open union so additional non-DID entity kinds can be added in the future."},
"app.certified.graph.getEntityFollow.entityFollowRecordView": {"type":"object","description":"Raw indexed entity-follow record view, including the DID that published the relationship.","required":["uri","cid","indexedAt","did","record"],"properties":{"uri":{"type":"string","format":"at-uri"},"cid":{"type":"string","format":"cid"},"indexedAt":{"anyOf":[{"type":"string","format":"datetime"},{"type":"null"}]},"did":{"type":"string","format":"did"},"record":{"$ref":"#/components/schemas/app.certified.graph.entityFollow.main"}}},
"app.certified.graph.getEntityFollow.output": {"type":"object","required":["follow"],"properties":{"follow":{"anyOf":[{"$ref":"#/components/schemas/app.certified.graph.getEntityFollow.entityFollowRecordView"},{"type":"null"}]}}},
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 @@ -37,6 +37,8 @@ Do not use `authors[]=` or comma-joined values unless that exact endpoint contra

A useful starting distinction: arrays often mean “any of these” for a filter, while separate filters may combine with AND, but this is **not a universal rule**. For example, the bundled collection contract says `tagUris` requires every supplied tag URI, while `itemUris` matches URI without CID. Confirm the exact operation before relying on filter logic.

`app.certified.feed.getReactionCounts` accepts 1–100 subject AT-URIs. Append each value under the unbracketed `subjects` key; do not use `subjects[]` or comma-join values. Its response has one entry per input occurrence in the same order, including duplicates. A zero count means no matching indexed reaction publishers were found at query time; counts may change as indexing changes.

## Keep page requests cursor-consistent

For a list/search operation whose output has a cursor, keep a base set of all non-cursor parameters and replace only the cursor on continuation. Preserve repeated array values, filters, `search`, `limit`, and `sortDirection` exactly. The example below is illustrative; verify the chosen endpoint's cursor binding and response-array name.
Expand Down
5 changes: 5 additions & 0 deletions .changeset/batch-reaction-counts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hypercerts-org/hypercerts-api': minor
---

Add a public query that returns like and repost counts for up to 100 subject records at a time.
Loading
Loading