BotanaryBotanary
Integration

Activity

The tamper-evident, hash-chained audit log - every attempted and executed action, cursor-paginated.

Activity is Botanary's audit trail. Its own tag description is exact about what it is: "the tamper-evident, hash-chained audit log (paginated; executed vs declined)."

Endpoints

  • GET /activity lists the log: "every attempted/executed action and every grant/revoke," cursor paginated. Query params: cursor, limit (1 to 100, default 25), status, delegationId, from/to (ISO-8601), and accountId. It returns an ActivityPage.
  • GET /activity/anchor reports how much of the log is committed on-chain. Its description explains exactly why that distinction matters: "unanchoredCount is the window of records written since the last anchor - tamper-evident (the hash chain) but not yet tamper-proof (nothing on-chain commits to them). Reported, never hidden." It returns an AnchorStatus.
  • GET /activity/{eventId} returns one AuditEvent, with its hash-chain fields.

On the backend, the anchor route is declared ahead of the {eventId} route deliberately, so a literal path like /activity/anchor doesn't get swallowed by the dynamic {eventId} matcher.

The AuditEvent shape

Fields: id, timestamp, optional dateLabel/timeLabel, actor, action, optional amount/token, chain, chainId, status, an optional declineReason, provenance, an optional txHash, an optional auditNonce (AgentGuard's own monotonic counter, present only on executed actions), an optional prevHash, hash, and an optional delegationId.

A real example from the contract, illustrating a declined action:

id: a2
status: declined
declineReason: per_action_exceeded
provenance: offchain_only
delegationId: d1

ActivityPage wraps a page of results as { items: AuditEvent[], pageInfo: PageInfo }, where PageInfo is { nextCursor?, hasMore } - the standard cursor-pagination shape used across the list endpoint. AnchorStatus requires available and unanchoredCount, with an optional anchoredHead hash.

When an entry gets written

An audit entry is appended exactly once per operation, at the moment it reaches a terminal status - never speculatively, never twice. See User operations for the submit/reconcile flow that guarantees that "exactly once."

On the frontend

The frontend's Activity page merges this log with a separate "My jobs" tab (marketplace jobs), filters between all, executed, and blocked, and paginates in small pages. Every audit event it renders is typed straight off the generated schema:

type AuditEvent = components['schemas']['AuditEvent'];

On this page