SDKs
Agents
Creating tier-3 / tier-4 tokens, handling the Authorization pause, verifying dual-attribution events. Side-by-side TypeScript, Python, and Go.
Last updated
TL;DR. Tier-3 tokens prepare mutations and pause via an Authorization resource
for a human signature. Tier-4 tokens execute autonomously. Each SDK surfaces the pause
as a typed MatterAuthorizationPendingError and the resume as an authorization.approved
event. Every event carries dual attribution — principal.human_id and principal.agent_id.
The four-tier agent model in Matter:
| Tier | Capability | Pauses for human? |
|---|---|---|
| 1 | Observe — read-only | Never |
| 2 | Prepare — drafts only, no commits | Never |
| 3 | Execute — mutates, but high-stakes actions pause | Yes, on the Authorization resource |
| 4 | Autonomous — no pauses | No (audit-only) |
Agent tokens (tok_…) carry a tier and a structured scope policy. Every event emitted
under an agent token records dual attribution — both the human principal and the agent
identity. See agents for the full model. The pause/resume protocol
follows the same "agent prepares, human approves, agent resumes" shape as the
OAuth 2.1 device-authorization extension.
When to use tier 3 vs tier 4
Tier 3 when the agent operates with a human in the loop — most production agent
deployments. Tier 4 when the agent operates for a human who's away — overnight
compliance automation, batch portfolio operations, scheduled M&A envelope advances.
Tier 4 is audit-only (the human can review but doesn't approve every action) so use it
sparingly and with a tight limits clause in the policy.
Creating a tier-3 token
Tier-3 is the most common shape — the agent does most of the work, but high-stakes mutations (formation authorization, dissolution, M&A advance) wait on a human.
const token = await matter.tokens.create({
tier: 3,
scopes: [
{
allow: ["entities.read", "filings.create", "documents.write"],
conditions: { jurisdiction: ["US-DE"] },
limits: { mutations_per_day: 50 },
},
],
principal: { human_id: "usr_4Kj2m8pQ", agent_id: "agt_Nq3KcAbc" },
api_version: "2026-05-01",
});
console.log(token.id); // tok_…
console.log(token.secret); // shown once — store securelytoken = matter.tokens.create(
tier=3,
scopes=[{
"allow": ["entities.read", "filings.create", "documents.write"],
"conditions": {"jurisdiction": ["US-DE"]},
"limits": {"mutations_per_day": 50},
}],
principal={"human_id": "usr_4Kj2m8pQ", "agent_id": "agt_Nq3KcAbc"},
api_version="2026-05-01",
)
print(token.id)
print(token.secret) # shown oncetoken, err := client.Tokens.Create(ctx, &matter.TokenCreateParams{
Tier: 3,
Scopes: []matter.ScopePolicy{{
Allow: []string{"entities.read", "filings.create", "documents.write"},
Conditions: map[string]any{"jurisdiction": []string{"US-DE"}},
Limits: map[string]int{"mutations_per_day": 50},
}},
Principal: matter.Principal{
HumanID: "usr_4Kj2m8pQ",
AgentID: "agt_Nq3KcAbc",
},
APIVersion: "2026-05-01",
})
if err != nil {
return err
}
fmt.Println(token.ID)
fmt.Println(token.Secret) // shown onceThe structured scope DSL — allow, deny, resources, conditions, limits — is
documented in agents. The SDK helpers do no
pre-validation beyond shape — the server is the source of truth.
Handling the Authorization pause
When a tier-3 agent attempts a high-stakes write, the SDK raises a typed pending error
carrying the Authorization resource ID. Surface this to your control plane —
typically by writing the auth ID to a queue that a human-facing UI polls.
import { MatterAuthorizationPendingError } from "@mattermode/node";
try {
const filing = await agent.filings.create({
entity: "ent_Nq3KcAbc",
form: "DE-AnnualReport",
});
console.log("filed:", filing.id);
} catch (err) {
if (err instanceof MatterAuthorizationPendingError) {
// Surface to the human. Action will execute when authorization is approved.
await queue.enqueue({
authorizationId: err.authorizationId,
summary: err.summary, // human-readable summary of the pending action
requestedBy: err.principal, // { human_id, agent_id }
});
} else {
throw err;
}
}from matter.errors import MatterAuthorizationPendingError
try:
filing = agent.filings.create(entity="ent_Nq3KcAbc", form="DE-AnnualReport")
print("filed:", filing.id)
except MatterAuthorizationPendingError as err:
queue.enqueue({
"authorization_id": err.authorization_id,
"summary": err.summary,
"requested_by": err.principal, # {human_id, agent_id}
})_, err := agent.Filings.Create(ctx, &matter.FilingCreateParams{
Entity: "ent_Nq3KcAbc",
Form: "DE-AnnualReport",
})
var pending *matter.AuthorizationPendingError
if errors.As(err, &pending) {
queue.Enqueue(QueueItem{
AuthorizationID: pending.AuthorizationID,
Summary: pending.Summary,
RequestedBy: pending.Principal,
})
} else if err != nil {
return err
}The Authorization resource carries:
summary— human-readable description of the pending action.target— the resource that would be created or mutated (withdry_runshape).principal—{human_id, agent_id}of the requesting agent.expires_at— the action lapses if not approved within this window.
Approving an Authorization
Two paths:
- Dashboard — the human signs in, reviews the pending action, clicks Approve. This is the usual flow.
- API —
POST /v1/authorizations/{id}/approvewith apk_live_or session token. Used by white-label receivers that build their own approval UI.
await matter.authorizations.approve("auth_4Kj2m8pQ", {
signature: humanSignature,
});matter.authorizations.approve("auth_4Kj2m8pQ", signature=human_signature)_, err := client.Authorizations.Approve(ctx, "auth_4Kj2m8pQ", &matter.AuthorizationApproveParams{
Signature: humanSignature,
})On approval, the original action executes server-side and emits an authorization.approved
event followed by the resource's terminal events.
Listening for authorization.approved
In your webhook receiver, react to the approval and resume any client-side state:
async function handle(event: Event) {
switch (event.type) {
case "authorization.approved":
// The action that was paused has now executed. The resource event will follow.
log.info("auth approved", {
authorizationId: event.data.id,
approvedBy: event.data.approved_by,
});
break;
case "filing.completed":
// The resource event that follows
break;
}
}Verifying dual attribution
Every event under an agent token carries principal: {human_id, agent_id}. Use this in
audit logs and access decisions — both the agent and the human are accountable.
function audit(event: Event) {
log.info("agent action", {
eventId: event.id,
type: event.type,
agent: event.principal.agent_id,
human: event.principal.human_id,
sequence: event.sequence,
});
}def audit(event):
log.info("agent action", extra={
"event_id": event.id,
"type": event.type,
"agent": event.principal.agent_id,
"human": event.principal.human_id,
"sequence": event.sequence,
})func audit(ev *matter.Event) {
log.Info("agent action",
"event_id", ev.ID,
"type", ev.Type,
"agent", ev.Principal.AgentID,
"human", ev.Principal.HumanID,
"sequence", ev.Sequence,
)
}AuditEntry (aud_…) records every state transition with the same dual attribution.
Pull client.AuditEntries.List(...) for the immutable historical record.
Tier-4 tokens
Tier-4 is autonomous — no Authorization pause. Reserve tier-4 for actions where the
human has explicitly delegated full authority and the blast radius is bounded by the
scope policy.
const token = await matter.tokens.create({
tier: 4,
scopes: [
{
allow: ["compliance.read", "filings.create"],
resources: { entities: ["ent_Nq3KcAbc"] }, // single-entity scope
conditions: { form: ["DE-AnnualReport", "BOI"] }, // only routine filings
limits: { mutations_per_day: 5 },
},
],
principal: { human_id: "usr_4Kj2m8pQ", agent_id: "agt_Compliance" },
api_version: "2026-05-01",
});A tier-4 token within a tight scope is the right shape for "autopilot the routine annual filings on this one entity." A tier-4 token with broad scopes is almost always a bug.
What's next
Webhooks
authorization.approved events and idempotent handlers.
Error handling
MatterAuthorizationPendingError and the rest of the typed hierarchy.