diff --git a/build-an-oracle/develop/identity-and-auth.mdx b/build-an-oracle/develop/identity-and-auth.mdx
index ede3c64..184a65e 100644
--- a/build-an-oracle/develop/identity-and-auth.mdx
+++ b/build-an-oracle/develop/identity-and-auth.mdx
@@ -207,10 +207,11 @@ handler: async (args, rtCtx: RuntimeContext) => {
return JSON.stringify({ error: 'Could not resolve downstream service DID.' });
}
- const invocation = await rtCtx.ucan.mintInvocation({
- did: serviceDid,
- capability: 'ixo:downstream',
- });
+ const invocation = await rtCtx.ucan.mintInvocation(
+ { did: serviceDid, capability: 'ixo:downstream' },
+ // Claim the ability the user's delegation grants — see below.
+ { can: 'downstream/*' },
+ );
const resp = await fetch('https://downstream-service.example/data', {
headers: {
@@ -226,7 +227,7 @@ The full UCAN helper surface on `rtCtx.ucan`:
| Method | Purpose |
| --- | --- |
-| `mintInvocation(target, opts?)` | Mint a service-targeted invocation from the user's cached delegation. `target` = `{ did, capability }`; `opts.skipCache` forces a fresh signature. |
+| `mintInvocation(target, opts?)` | Mint a service-targeted invocation from the user's cached delegation. `target` = `{ did, capability }`; `opts.can` is the ability claimed (default `'*'`); `opts.skipCache` forces a fresh signature. |
| `requireCapability(resource, action)` | Throws if the user's delegation doesn't include the capability. |
| `hasCapability(resource, action)` | Returns a boolean — non-throwing variant. |
| `resolveServiceDid(serviceUrl)` | Resolves a service URL to its `did:web:...` identifier. Returns `null` when the DID document is missing or has no `id`. |
@@ -235,6 +236,39 @@ The full UCAN helper surface on `rtCtx.ucan`:
See [`packages/oracle-runtime/src/modules/ucan/`](https://github.com/ixoworld/ixo-oracles-boilerplate/tree/main/packages/oracle-runtime/src/modules/ucan) for the service implementation.
+### Claim only what you were granted
+
+An invocation says what the oracle is *doing right now*; the delegation says what the user *permitted*. The service accepts the invocation only if the delegation covers it — and coverage is narrower than it looks. A granted ability covers a claim when it is:
+
+- `'*'` — covers everything, or
+- **exactly equal** to the claim, or
+- a `prefix/*` pattern matching it (`memory/*` covers `memory/read`).
+
+Nothing else. In particular **`'*'` is not a wildcard when you *claim* it** — a `'*'` claim is satisfiable only by a `'*'` grant, because `'*'` does not start with `memory/`:
+
+```ts
+// delegation grants { can: 'memory/*', with: 'ixo:memory' }
+
+// ❌ over-claim — refused: "Delegated capability not found"
+await rtCtx.ucan.mintInvocation({ did, capability: 'ixo:memory' });
+
+// ✅ claims exactly what was granted
+await rtCtx.ucan.mintInvocation(
+ { did, capability: 'ixo:memory' },
+ { can: 'memory/*' },
+);
+```
+
+
+The service must **register** the ability you claim. It matches an invocation's `can` by strict string equality, so a service that only defines `'*'` rejects a `memory/*` invocation as an unknown capability *before* authorization is considered. When narrowing a claim, roll out in this order:
+
+1. service accepts the narrow ability **and** `'*'`
+2. oracle switches to claiming the narrow ability
+3. service tightens or upgrades
+
+Reversing steps 1 and 2 produces a 401 on every call.
+
+
## What plugins can and cannot do
- **Can:** read `rtCtx.user.did`, `rtCtx.user.matrixUserId`, `rtCtx.user.ucanDelegation`, `rtCtx.user.timezone`.
diff --git a/build-an-oracle/reference/runtime-context.mdx b/build-an-oracle/reference/runtime-context.mdx
index ee81b43..2383cb2 100644
--- a/build-an-oracle/reference/runtime-context.mdx
+++ b/build-an-oracle/reference/runtime-context.mdx
@@ -56,7 +56,7 @@ export interface RuntimeContext {
ucan: {
requireCapability: (resource: string, action: string) => void;
hasCapability: (resource: string, action: string) => boolean;
- mintInvocation: (target: { did: string; capability: string }, opts?: { skipCache?: boolean }) => Promise;
+ mintInvocation: (target: { did: string; capability: string }, opts?: { skipCache?: boolean; can?: string }) => Promise;
resolveServiceDid: (serviceUrl: string) => Promise;
hasSigningKey: () => boolean;
createInvocationFromDelegation: (
@@ -171,7 +171,21 @@ export interface RuntimeContext {
- `requireCapability(resource, action)` — throws if the user's delegation doesn't include this capability.
- `hasCapability(resource, action)` — boolean check.
- - `mintInvocation({ did, capability }, opts?)` — mint a downstream invocation signed by the oracle's signing mnemonic.
+ - `mintInvocation({ did, capability }, opts?)` — mint a downstream invocation signed by the oracle's signing mnemonic. `opts.can` is the **ability** the invocation claims (default `'*'`); `opts.skipCache` bypasses the invocation cache, required for services that enforce single-use replay protection per invocation CID.
+
+
+ **Claim the ability the user's delegation actually grants.** A claim resolves against a delegation only when the granted ability is `'*'`, equals the claim, or is a `prefix/*` covering it. So the default `'*'` claim is satisfiable **only** by a `'*'` grant — if the user granted `memory/*`, a `'*'` claim is an over-claim and the service refuses it:
+
+ ```ts
+ // ✅ delegation grants { can: 'memory/*', with: 'ixo:memory' }
+ await rtCtx.ucan.mintInvocation(
+ { did: memoryDid, capability: 'ixo:memory' },
+ { can: 'memory/*' },
+ );
+ ```
+
+ The service must also register that ability: it matches an invocation's `can` by **strict equality**, so one that only defines `'*'` rejects a `memory/*` invocation as an unknown capability before authorization is considered. Roll out the service side first.
+
- `resolveServiceDid(serviceUrl)` — look up a downstream service's DID document; returns `id` or `null`.
- `hasSigningKey()` — `true` once the oracle has loaded its Ed25519 signing mnemonic. **Gate registration of mint-capable tools on this**: without a key, minting is a no-op, so the tool should surface an error rather than pretend it worked.
- `createInvocationFromDelegation(delegationCar, serviceUrl, capability, options?)` — mint an invocation from a **directly-supplied** delegation CAR (rather than the user's cached one), targeted at a specific service route. Returns `{ invocation }` on success or `{ error }` with a surfaced-verbatim reason (missing signing key, audience mismatch, did:web unreachable, …).