at-intents docs producers.md
6.7 kB · 159 lines
Markdown
at main

Producer guide — publish your app's capabilities #

You're a producer if your app does things: saves, subscribes, shares, annotates, opens. This guide takes you from "my app has features" to "other apps and agents can discover and route to those features from a user's repo."

New to the model? Read the overview first. Want an agent to do the analysis for you? See the agent guide.

There are two jobs:

  1. Publish capability records (once, in your app's repo) — describe what you can do.
  2. Write usage records (per user, in their repo) — leave the footprint that lets consumers discover you at all.

1. Publish your capabilities #

Step 1 — enumerate your write/read surfaces #

List everything a user can do through your app. For each, you'll fill in the triple plus delivery. Ask:

  • verb — is it share, save, subscribe, annotate, open, or something new? Don't force everything into "share."
  • subject — what does it act on? A bare URL (uri)? A record of a given type (at-uri + of)? An account (did)? List all accepted shapes — it's a union.
  • delivery — the key decision, below.

Step 2 — choose delivery for each surface #

This is the field that decides whether a capability actually works. Pick by asking where the canonical data lives:

If… delivery You must…
your app just reads a primitive the user already has in their repo passive nothing else — no produces, no scope
your app's data lives in the user's PDS as a record you read repo list produces (the NSID you write) and a scope
your app's canonical store is your own backend (a PDS write won't reach you) service give an XRPC endpoint (https://host/xrpc/<nsid>); add serviceId (your DID-doc service fragment) so consumers can reach it by service proxying

The classic mistake: marking a backend-canonical save as repo or passive. A record written to the user's repo never reaches your backend, so the capability looks free and silently fails. Use service.

Step 3 — write the record as JSON #

A capability is just a JSON record matching the lexicon — $type and createdAt are filled in for you. Full field reference: capability-spec.md.

{
  "name": "My Reader",
  "icon": "https://myreader.app/icon.png",
  "description": "Subscribe to a feed or account so new items appear in your reading list. Use to follow a source over time; not for saving a single article.",
  "verb": "subscribe",
  "subject": [
    { "kind": "uri" },
    { "kind": "did", "as": "account" }
  ],
  "produces": ["app.myreader.subscription"],
  "delivery": "repo",
  "scope": "include:app.myreader.subscribe"
}

Write description as if a planner will read it: what it does, when to use it, what it won't do. Tool-calling quality is dominated by description quality, and the same text feeds a human picker.

See tools/examples/ for one record per delivery type and a multi-capability file.

Step 4 — validate and write #

You'll need an app password for your app's account (on Bluesky: Settings → Privacy and security → App passwords — never your main password). The tooling is zero-dependency Node 18+ — no install.

cd tools

# Always dry-run first: validates and prints the exact write, but writes nothing.
node write-capability.mjs examples/subscribe.capability.json --dry-run \
  --identifier myreader.app

# Write for real (capability records go in YOUR app's repo):
PDS_IDENTIFIER=myreader.app PDS_APP_PASSWORD=xxxx-xxxx-xxxx-xxxx \
  node write-capability.mjs my-subscribe.capability.json

Prefer to be walked through it? node write-capability-interactive.mjs prompts for each field, saves a reusable JSON record, shows a dry-run, and writes on confirmation.

Publish your whole surface at once with a { "capabilities": [...] } file — see examples/all.capabilities.json.

Step 5 — make the lexicon resolvable (optional, for live discovery) #

Capability records live in your repo and resolve today. For the lexicon NSID to resolve on the open network, the authority domain (here at-intent.dev) must publish the lexicon per atproto lexicon resolution. Until then, consumers fetch records directly by collection NSID from your PDS — which the resolver already does. Nothing blocks you from publishing records now.


2. Write usage records (the footprint) #

A capability nobody can discover is invisible. When a person uses your app, write a dev.at-intent.usage record into their repo so consumers find you:

{
  "$type": "dev.at-intent.usage",
  "app": "did:web:myreader.app",
  "config": { "defaultCategory": "tech" },
  "lastSeenAt": "2026-06-26T12:00:00Z",
  "createdAt": "2026-03-01T00:00:00Z"
}
  • You already have write access to the user's repo (they authed your app), so you can create this during normal use.
  • Dedupe by app — one usage record per handler is enough.
  • Bump lastSeenAt when they use the app again, so consumers can age out stale handlers (default TTL ~180 days).
  • config is your private per-user blob (a default collection, a category) — shape is yours.

For manual/testing creation (writes into the user's repo, so auth as the user):

node write-usage.mjs --app did:web:myreader.app \
  --config '{"defaultCategory":"tech"}' --dry-run --identifier alice.bsky.social

Checklist #