> ## Documentation Index
> Fetch the complete documentation index at: https://docs.soundchecklive.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Creator attribution and agent-created share

> Who created each gig, shift, offer, message and payout (a person, CheckAI, an external agent or an import), the sor_write signal, and the metric definitions built on them.

PRO-651 (child 1 of the agent-ready ops layer, PRO-650). Agent-created share is
that epic's metric. It sits under the company north star, trusted book rate
(PRO-397), and does not replace it.

## What is recorded

Seven tables carry the same seven columns:

| Object | Table | `sor_write` `object_type` |
| - | - | - |
| Gig | `Event` | `gig` |
| Crew demand | `event_crew_requirements` | `crew_demand` |
| Shift (a crew booking on a gig) | `Invitation` | `shift` |
| Crew offer | `crew_offers` | `offer` |
| Message (SMS / email send) | `Notification` | `message`, once per gig per request |
| Payout | `ledger_items` | `payout`, `PAYABLE` lines only |
| Payment | `transactions` | `payment` |

| Column | Meaning |
| - | - |
| `created_by_kind` | `PERSON`, `CHECKAI`, `EXTERNAL_AGENT`, `IMPORT` or `SYSTEM` |
| `created_by_user_id` | The human principal: who the person was, or who the agent acted for |
| `created_by_agent_id` | Which agent: `checkai`, `oauth:<client id>`, `soundcheck-mcp-sdk` |
| `created_via` | The rail: `app`, `chat`, `checkai`, `mcp`, `import`, `system` |
| `last_modified_by_kind` / `_user_id` / `_agent_id` | The same facts for the latest real change |

**The server sets them, never the client.** A `BEFORE INSERT OR UPDATE` trigger
(`creator_attribution_stamp`) copies them from the request-scoped `app.audit_ctx`
setting that every attributed write already carries (see
[Audit actor model](https://docs.soundchecklive.io/engineering/backend/audit-actor-model.md)). A value the client writes into
these columns is overwritten. An update keeps the creator columns from the old
row. An update that only touches bookkeeping columns (`updated_at`, and the
table's `ignore_columns` in `audit.table_config`, such as sync claims) is not a
change: it leaves the modifier alone and emits nothing.

**An edit stored on a child table is an edit to its object.** An offer's
per-day fees, seats and times live on `crew_offer_days`. A non-system write to a
day (insert, update or delete) marks the parent offer through the
transaction-local `app.creator_attribution_touch` setting and touches it, so the
offer records the modifier and queues its `update` under the same per-request
key. Bookkeeping-only day updates (`times_changed_at`) do not count. Crew
responses (`crew_offer_recipients`, `crew_offer_recipient_days`) are not edits to
the offer and are not attached.

**NULL means "not measured".** Rows written before the migration
(`20261103090000_creator_attribution`) keep NULL attribution. Nothing guesses it.

## How the kind is decided

`creatorKindOf` (`api-events/src/systems/audit/auditContext.ts`) maps the audit
actor to a kind. The derivation happens where the caller is known:

| Caller | Where | Actor | Kind | Agent | Rail |
| - | - | - | - | - | - |
| A signed-in person in web or mobile | `auditContextMiddleware` | `USER` | `PERSON` | — | `app` |
| CheckAI with a Slack delegation token | `auditContextMiddleware` | `CHECKAI` | `CHECKAI` | `checkai` | `checkai` |
| In-app CheckAI with a valid `Soundcheck-Agent-Assertion` | `auditContextMiddleware` | `CHECKAI` | `CHECKAI` | `checkai` | `chat` |
| `Soundcheck-Client: mcp` with no valid assertion (the stdio `@soundcheck/mcp` SDK, or a spoofed header) | `auditContextMiddleware` | `INTEGRATION` | `EXTERNAL_AGENT` | `soundcheck-mcp-sdk` | `mcp` |
| `/mcp` with an OAuth access token (`client_id` claim) | `enterMcpAuditContext` | `INTEGRATION` | `EXTERNAL_AGENT` | `oauth:<client_id>` | `mcp` |
| `/mcp` with a first-party session token (no `client_id`) | `enterMcpAuditContext` | `CHECKAI` | `CHECKAI` | `checkai` | `checkai` |
| `commitImport` / `commitIngestion` (spreadsheet, UEF, URL) | `runAsImport` | unchanged | `IMPORT` | unchanged | `import` |
| Webhooks, jobs, syncs, scripts, no context | trigger default | `SYSTEM` / `WEBHOOK` | `SYSTEM` | — | `system` |

**The CheckAI assertion.** CheckAI calls the REST API with the user's own Clerk
token, so the token cannot say an agent is acting. CheckAI therefore signs each
request: `v1.<unix seconds>.<c0|c1>.<HMAC>` over the timestamp, the confirm flag
and a SHA-256 of the bearer token. The key is derived from
`SLACK_CHANNEL_CHECKAI_SECRET`, which both services already hold, under its own
label. An assertion is valid for 5 minutes (60 s of clock skew) and only with
the token it was minted for. Without a valid one, a `Soundcheck-Client: mcp`
call is attributed to an external agent: the fallback is "not ours".

**External agent identity.** Until AgentID (PRO-579 / PRO-581) lands, an
external agent is identified by its OAuth client (`oauth:<client_id>`).

**An agent-driven import is an import.** `runAsImport` sets the creator kind
and rail but keeps the actor, so the audit log still says CheckAI ran it.

## `sor_write`

One PostHog event per object written per request. The same trigger tables run an
`AFTER INSERT OR UPDATE` trigger (`creator_attribution_sor_write`) that queues an
`effect_outbox` row of kind `sor_write`, keyed
`sor_write:<write scope>:<object type>:<row id>` with `ON CONFLICT DO NOTHING`.
The write scope is a server-generated id per request (`writeScopeId`), never a
client-supplied correlation id:

* **Once per write.** A create plus edits in the same request are one `create`.
  A retried HTTP request is a new request; it emits again only if it writes
  again (duplicate creates are ENG-1232, not a signal problem).
* **Never for a rolled-back write.** The outbox row is in the writer's
  transaction.
* **Never duplicated on delivery.** The outbox row id is the PostHog `uuid`, so
  a redelivered batch is deduplicated by PostHog.
* **Not for `SYSTEM` writes**, nor for a message with no gig (no org to file it
  under), nor for ledger lines other than `PAYABLE`.
* **One message per send.** A send to 40 crew writes 40 `Notification` rows and
  one `message` event per gig.

The effect sweep (`effectOutboxSweepHandler`, every 5 minutes) delivers these
rows in their own lane, `runSorWriteEffects`: up to 20 batches of 100 per tick
through PostHog `/batch/`. Accepted rows are settled `DONE` and kept for a
day, so a later write in the same request still hits the key, then the lane
prunes them. Rows that exhaust their retries are dead-lettered and reported to
Sentry like any other effect. The shared lane skips `sor_write` so analytics
volume never delays calendar, Twilio or email effects. With no
`POSTHOG_API_KEY` the rows are settled without being sent.

`confirm_gated` is true when the write followed an accepted MCP confirmation
token in the same tool call (`requireConfirmation` marks the call; see
`toolCallConfirmed` in `@soundcheck/mcp`). For CheckAI the flag travels inside
the signed assertion.

Properties are listed in the
[analytics taxonomy](https://docs.soundchecklive.io/engineering/reference/analytics-taxonomy.md).

## Metric definitions (draft, pending Product Analytics sign-off)

These definitions are the proposal for Product Analytics to sign off. Until they
do, no dashboard number is quoted as a baseline.

**System-of-record write.** One `sor_write` event. Only `person`, `checkai`,
`external_agent` and `import` writes exist as events. `system` writes are not
emitted.

**Agent-created share (weekly).**
`count(sor_write where operation = create and agent_created)` ÷
`count(sor_write where operation = create)`, per ISO week, overall, by `org_id`
and by `object_type`, split by `creator_kind`. Internal and test orgs must be
excluded; which cohort or org property does that is part of the sign-off.
Writes on sample gigs (`Event.is_sample`) should be excluded too. "Not reverted
within 7 days" is applied once the activity log with undo (PRO-653) records
reverts. Until then the insight is labelled "before revert exclusion".

**Completed gig.** A gig whose status reaches completed or settled
(`event_completed` / `event_settled`) in the window.

**Clicks per completed gig.** PostHog `$autocapture` clicks on web and mobile
`capture` interactions carrying that gig's `event_id`, from the gig's
`event_created` to its settle, by operator roles only (crew excluded).

**Active minutes per completed gig.** The number of distinct minutes, in the same
create → staff → pay window, in which an operator sent at least one interaction
for that gig. A minute with no interaction is idle and not counted.

**Guardrail line.** Median and p75 active minutes per completed gig against the
existing under-10-minutes goal (PRO-540), split by gigs with at least one agent
`sor_write` and gigs with none. No other target is set.

**Baselines** are recorded as "measured on `<date>`" once the dashboard has
8 weeks of data. None is quoted before then.

## Not in this change

* The PostHog dashboard and insights (agent-created share, clicks and minutes
  per completed gig). They need the definitions above signed off and PostHog
  project access.
* Client instrumentation for clicks and active minutes. Web and mobile
  interactions need the gig's `event_id` on them before the per-gig counts
  above are computable.
* The 7-day revert exclusion (PRO-653).
* Sub-rails for the app (`templates`, `staffing` in PRO-402). Person writes are
  `app` until the routes that own them set a rail.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.