What
jspod already supports giving an autonomous agent scoped, key-authenticated read/write to a pod — no OAuth, no JWT, just a Nostr key. We dogfooded the full loop and it works cleanly, but the recipe isn't documented and we hit a few avoidable papercuts. Filing so it can become docs (and maybe a couple of small DX fixes).
The pattern
An agent holds its own Nostr key. It reads a pod resource publicly and writes with a NIP-98 (kind 27235) Schnorr signature. The server resolves the signing pubkey to an identity two ways:
- As a WebID — if the pubkey is published in the pod owner's profile as a CID v1
verificationMethod referenced from authentication. jspod's profile already ships the cid / lws context with the verificationMethod / publicKeyMultibase / publicKeyJwk slots — they're just empty by default.
- As
did:nostr:<pubkey> — the fallback when the key isn't in that pod's owner profile (the cross-pod / "guest" case). Grant this identity in a WAC ACL.
Recipe
- Add the agent's Nostr key to a WebID profile as a Multikey
verificationMethod, listed in authentication. Encoding for publicKeyMultibase: f + e701 + 02 + the x-only pubkey hex.
- To let the agent write a resource it doesn't own, add a resource
.acl granting acl:agent = did:nostr:<pubkey> the acl:Write mode.
- Sign each write: a NIP-98 event with
u + method tags, sent as Authorization: Nostr <base64(event)>.
- General edit: the agent does
GET resource, modifies the JSON-LD, then PUTs it back signed. Two primitives (get / put) plus the agent's own reasoning give full CRUD — no per-verb endpoints needed.
Papercuts worth documenting / fixing
- The
.acl must live in the live --root. Writing it into a same-named pod-data/ directory elsewhere silently no-ops, because the server reads the runtime data root. Easy to get wrong when multiple checkouts exist.
- NIP-98
payload tag vs body re-serialization. The verifier hashes the parsed-and-re-serialized request body, so a payload SHA-256 computed over the client's exact bytes mismatches. Omitting the optional payload tag works (the signature still binds URL + method + freshness), but this should be documented — or the verifier should hash the raw body.
--no-bootstrap pods render blank in a browser. The data-browser assets (data-browser-folder.js / .css) aren't placed in /public, so an HTML GET 404s them and shows nothing. Either document "it's headless, copy these in," or serve them from the package regardless of bootstrap.
- A resource
.acl overrides inherited container defaults. You must restate the owner's Control and public Read alongside the new grant, or they're lost for that one resource.
acl:agent did:nostr matching is exact-string — worth a note so people get the casing/format right.
Suggestion
A small CLI helper would turn this from "hand-edit JSON-LD" into one command, e.g. jspod profile add-nostr-key <pubkey> and/or jspod grant <webid-or-did> <mode> <resource>.
What
jspod already supports giving an autonomous agent scoped, key-authenticated read/write to a pod — no OAuth, no JWT, just a Nostr key. We dogfooded the full loop and it works cleanly, but the recipe isn't documented and we hit a few avoidable papercuts. Filing so it can become docs (and maybe a couple of small DX fixes).
The pattern
An agent holds its own Nostr key. It reads a pod resource publicly and writes with a NIP-98 (kind
27235) Schnorr signature. The server resolves the signing pubkey to an identity two ways:verificationMethodreferenced fromauthentication. jspod's profile already ships thecid/lwscontext with theverificationMethod/publicKeyMultibase/publicKeyJwkslots — they're just empty by default.did:nostr:<pubkey>— the fallback when the key isn't in that pod's owner profile (the cross-pod / "guest" case). Grant this identity in a WAC ACL.Recipe
verificationMethod, listed inauthentication. Encoding forpublicKeyMultibase:f+e701+02+ the x-only pubkey hex..aclgrantingacl:agent=did:nostr:<pubkey>theacl:Writemode.u+methodtags, sent asAuthorization: Nostr <base64(event)>.GETresource, modifies the JSON-LD, thenPUTs it back signed. Two primitives (get/put) plus the agent's own reasoning give full CRUD — no per-verb endpoints needed.Papercuts worth documenting / fixing
.aclmust live in the live--root. Writing it into a same-namedpod-data/directory elsewhere silently no-ops, because the server reads the runtime data root. Easy to get wrong when multiple checkouts exist.payloadtag vs body re-serialization. The verifier hashes the parsed-and-re-serialized request body, so apayloadSHA-256 computed over the client's exact bytes mismatches. Omitting the optionalpayloadtag works (the signature still binds URL + method + freshness), but this should be documented — or the verifier should hash the raw body.--no-bootstrappods render blank in a browser. The data-browser assets (data-browser-folder.js/.css) aren't placed in/public, so an HTMLGET404s them and shows nothing. Either document "it's headless, copy these in," or serve them from the package regardless of bootstrap..acloverrides inherited container defaults. You must restate the owner'sControland publicReadalongside the new grant, or they're lost for that one resource.acl:agentdid:nostr matching is exact-string — worth a note so people get the casing/format right.Suggestion
A small CLI helper would turn this from "hand-edit JSON-LD" into one command, e.g.
jspod profile add-nostr-key <pubkey>and/orjspod grant <webid-or-did> <mode> <resource>.