Sub-delegation (v1.1)
OC Agent v1.1 adds sub-delegation as an additive extension to the v1.0 spec. An agent holding a delegation can issue a narrower delegation to a sub-agent, who in turn signs actions citing the leaf subdelegation. Verifiers walk the chain back to a root v1.0 delegation and enforce strict containment at every link.
Normative spec:
SUB-DELEGATION.md. This page is a friendly summary; consult the spec for the bit-level details.
Why
Real organizations issue authority hierarchically. A treasurer grants the finance bot Lightning-spending rights; the finance bot delegates to a vendor-specific bot for one supplier. Without sub-delegation, you'd have to re-issue every leaf grant directly from the root principal — defeating the point of having a finance bot at all.
Sub-delegation lets you build agent topologies that mirror real-world authority chains while keeping every action transitively bound to the root principal's Bitcoin identity.
The shape
Root principal (bc1qceo…)
│
│ delegates `ln:send(max_sats<=10000)`, 90 days
▼
Finance bot (bc1qfin…) ← v1.0 delegation
│
│ sub-delegates `ln:send(max_sats<=1000, node=03abc…)`, 7 days
▼
Vendor bot (bc1qvendor…) ← v1.1 sub-delegation
│
│ signs an action exercising `ln:send(max_sats=850, node=03abc…)`
▼
agent-action envelope ← cites the LEAF subdelegation
A verifier given the action walks the chain [root delegation, sub-delegation]
and enforces:
| At each link | Check |
|---|---|
| Linkage | child.parent_id == parent.id AND child.principal == parent.agent |
| Scope | every scope in child.scopes is a sub-scope of some scope in parent.scopes |
| Time | child.issued_at >= parent.issued_at AND child.expires_at <= parent.expires_at |
| Authenticity | child carries a valid BIP-322 signature by child.principal.address |
| Revocation | no published revocation against this link predates the action |
If any check fails at any link, the action fails verification. The leaf-level checks (action's signer, window, scope) are run against the leaf subdelegation, not the root.
Strictly narrowing
Sub-delegations CAN'T expand authority. The spec rules this out structurally:
- Scope. A child scope's constraints must transitively contain the action's
scope, AND must be a sub-scope of some parent scope. So
lock:seal(recipient=bc1qalice)(parent) →lock:seal(recipient=bc1qmallory)(child) is rejected withE_SUBDELEGATION_SCOPE_ESCALATED— same key, same op, different value. - Time. A child's window must be inside the parent's.
expires_atpast the parent's →E_SUBDELEGATION_EXPIRES_EXTENDED. - Identity. A child's
principal.addressmust equal the parent'sagent.address. Anyone else trying to sub-delegate from a parent they don't control →E_SUBDELEGATION_PRINCIPAL_MISMATCH.
There's no way for a compromised sub-principal to grant more than they hold.
Bond stays at the root
Sub-delegations carry no bond field. The root delegation's bond backstops the entire chain. The principal's reputational stake is committed once, on the grant, and the verifier re-resolves it at action evaluation time — exactly like v1.0.
If you need a sub-agent to stake their own sats independently, that's a different verb (and a deferred v2 question).
Revocation cascades
Revoking ANY link in the chain invalidates that link AND every descendant. The
verifier implements this naturally — it queries Nostr kind-30085 for revocations
targeting every link in the chain (one filter,
{ kinds: [30085], '#t': [<root id>, <link 1 id>, …] }), and any revocation
predating the action's effective time fires E_REVOKED.
If the root principal wants to nuke an entire sub-tree, they revoke the root delegation. If a sub-principal wants to disown one branch they created, they revoke just that subdelegation.
Depth limit
Verifiers cap chain depth (default 5). Chains beyond that fail fast with
E_SUBDELEGATION_DEPTH_EXCEEDED. Keeps the verifier's worst-case work bounded
and makes "publish a million subdelegations to slow the verifier" non-attack.
Wire format
A sub-delegation is the same JSON shape as a v1.0 delegation, with one extra
field (parent_id) and no bond field. The canonical message domain separator
is oc-agent:subdelegation:v1 (so signatures can't be replayed against v1.0
delegations).
{
"v": 1,
"kind": "agent-subdelegation",
"id": "<64-hex>",
"parent_id": "<64-hex>",
"principal": { "address": "<= parent.agent.address>", "alg": "bip322" },
"agent": { "address": "<sub-agent>", "alg": "bip322" },
"scopes": ["<each must be a sub-scope of some parent scope>"],
"issued_at": "<>= parent.issued_at>",
"expires_at": "<<= parent.expires_at>",
"nonce": "<32-hex>",
"revocation": { "holders": ["principal"], "ref": null },
"sig": {
"alg": "bip322",
"pubkey": "<principal.address>",
"value": "<base64>"
}
}
Published to Nostr kind 30086 with d-tag prefix oc-agent-sub:. Since
spec 1.5.0, publishers SHOULD add two t tags: the parent's id and the root
delegation's id, as 64 lowercase hex (one tag when they are equal). The event
also carries parent, root, principal, agent, expires, and one scope
per granted scope, for people reading it. Tags sit on the Nostr event, outside
the signed envelope, so they change no id.
Relays index single-letter tags only, so d and t are the query keys:
- One sub-delegation by id:
{ kinds: [30086], '#d': ['oc-agent-sub:<id>'] }. Anyone can publish under anyd, so use the event whose envelope id matches and verifies, not the newest. - Under a delegation
D:{ kinds: [30086], '#t': ['<D id>'] }. Keep direct children bycontent.parent_id; a deeper link matched by its root id counts only once its chain walks back toD.
The #t query finds only sub-delegations published with the tags, so an empty
answer never proves there are none. A verifier still gets the chain it judges
from whoever presents the action, or walks it link by link by id. #parent,
#root and #agent filters are rejected by most relays.
Backwards compatibility
A v1.0 verifier:
- Doesn't subscribe to kind 30086 → ignores subdelegations entirely.
- Sees an action whose
delegation_idresolves only to a kind-30086 event → looks it up at kind 30083, finds nothing, failsE_DELEGATION_MISMATCH. Correct fail-closed behavior.
A v1.1 verifier handling a v1.0 envelope (no chain): runs the same algorithm with chain length 1 (just the root). Identical verdict to v1.0. None of the existing test vectors v01–v09 change verdict under v1.1.
Where to dig deeper
- SUB-DELEGATION.md — normative spec.
- SPEC.md — base v1.0 spec.
@orangecheck/agent-core— reference TypeScript impl.verifyAction({ subdelegationChain: [...] })is the chain-walking entry point;verifySubdelegationis the standalone helper for inspection.- Test vectors
v10–v14inoc-agent-protocol/test-vectors/. - Errors — the four new v1.1 codes (
E_SUBDELEGATION_*) live alongside the existing v1.0 codes.