For use with Omni or any browser-driving agent. Everything you need is inline below — no external repo fetches required. The originating files (private repo) are listed at the end for reference, but you do NOT need to fetch them.
This single file contains, in order:
- §A — Task overview + build sequence (what to build, in what order)
- §B — Webhook receivers spec (the integration with the GitHub admission gate)
- §C — Schema spec (7 tables, every field, every formula)
- §D — Views spec (per-role views)
- §E — Interfaces spec (Interface Designer pages, with ASCII mockups)
- §F — Automations spec (A1–A12, the full event-driven logic)
- §G — Acceptance criteria + handoff
If you're an AI agent: read all of §A–§G before starting any action in Airtable. Then execute in the order above. Reply with a build-progress checklist after each phase.
You are building the Codebase Atlas bounty + compensation Airtable base. This base is the operational home for everything that happens after a task is admitted via the GitHub admission gate: hunters filing findings, triagers grading them, solvers attempting Hard / Reference tasks, payouts settling. The GitHub admission gate POSTs webhooks to this base when tasks admit and when their 7-day bounty windows close; you wire those webhooks into the right tables and Automations.
A single Airtable base named "Codebase Atlas Bounty (v1)" containing:
- 7 core tables built exactly per §C below
- 18 views per §D
- 10 Interface pages per §E
- 12 Automations (A1–A12) per §F
- Two webhook-receiver Automations per §B (not in §F — these are new for the GitHub admission gate integration)
Build order:
- Tables (§C) — leaf nodes everything else links to. Build
TasksandContributorsfirst; thenEscrow Ledger; thenFindings,Appeals,Arbitrations; thenAudit Loglast. - Permissions matrix (§C inline) — apply R/W per role before adding any test data.
- Views (§D) — bind each view to the right table with the documented filter/sort/group.
- Interfaces (§E) — build the 10 Interface Designer pages; bind to views; gate permissions per page.
- Automations (§F) — wire A1–A12; test each on synthetic data before going to next.
- Webhook receivers (§B) — set up the incoming webhook URL, branch on payload.event, route to A-admit and A-window-close logic.
- Acceptance test (§G) — end-to-end smoke test with synthetic + real-admission payloads.
Trigger: Incoming webhook from a GitHub Actions secret (AIRTABLE_ADMISSION_WEBHOOK_URL). One webhook URL handles both event kinds; the receiver branches on payload.event.
Payload:
{
"event": "admit",
"task_slug": "rustfs-iam-proxy-security",
"task_id": "T002",
"author_github_handle": "kurry",
"tier": "Hard",
"author_fee_usd": 750,
"p_solve_ensemble": 0.12,
"cross_family_passes": true,
"stage3_required": true,
"harbor_config_sha256": "9f3a1b2c...",
"admitted_at": "2026-06-15T12:34:56Z",
"admission_json_url": "https://github.com/.../admission.json"
}Actions:
- Idempotency check. Reject (200 OK + no-op) if a
Tasksrow already exists withslug = payload.task_slugANDadmitted_at = payload.admitted_at. - Resolve author. Find or create a
Contributorsrow matchingpayload.author_github_handle. If new, initializequality_score = 1.0,roles_active = ["Author"]. - Create
Tasksrow with: slug, task_id, author (link to Contributor), tier, author_fee_usd, p_solve_ensemble, cross_family_passes, stage3_required, harbor_config_sha256, shipped_at = admitted_at, bounty_status = "Open", admission_json_url. - Book the author fee. Append to the Mercor contributor-payment intake queue for
payload.author_fee_usdtoauthor_github_handle. - Write
Audit Logentry:event_type = "task_admitted", all payload fields recorded. - Reply 200 OK. On failure, reply 5xx so GitHub's webhook retry logic fires.
Payload:
{
"event": "window_close",
"task_slug": "rustfs-iam-proxy-security",
"task_id": "T002",
"shipped_at": "2026-06-15T12:34:56Z",
"closed_at": "2026-06-22T00:00:00Z",
"stage3_outcome": "cleared",
"final_tier": "Hard",
"final_author_fee_usd": 750
}stage3_outcome ∈ {cleared, dropped_one_tier, defaulted_medium, defaulted_easy, rejected_cheat}.
Actions:
- Idempotency check. Reject if
Tasks.bounty_statusis already"Closed". - Find
Tasksrow byslug = payload.task_slug. - Update: bounty_status = "Closed", closed_at, final_tier, final_author_fee_usd, stage3_outcome.
- If
final_tier ≠ tier(Stage 3 fell back), reconcile the author-fee booking — clawback the difference. Clawback infrastructure already exists in §F. - For accepted Findings against this task, settle deductions per §F. Per-task deduction is uncapped; residuals become clawback debts.
- For Hard / Reference with
stage3_outcome != cleared, mark the task as eligible for Hard re-admission — author has 7 days to file a re-admission request, which fires a new GitHub PR + admission run. - Write
Audit Logentry:event_type = "task_window_closed". - Reply 200 OK.
- Verify request origin via
X-Hub-Signature-256HMAC. Configure shared secret env var on GitHub side (AIRTABLE_WEBHOOK_HMAC_SECRET); verify HMAC-SHA256 of request body matches header. Reject 401 if not. - HTTPS only.
- Airtable Automations native rate-limiting must be enabled.
This file is the build spec. Anyone setting up the v1 base creates these seven tables with exactly these fields. Field-by-field. Formulas given verbatim where applicable.
Supersedes the schema fragments in files 14 and 16.
| # | Table | Purpose | Approx rows in steady state |
|---|---|---|---|
| 1 | Tasks |
One row per shipped task; canonical bounty target | 100–500 |
| 2 | Contributors |
One row per real-identity contributor; escrow + reputation | 50–200 |
| 3 | Findings |
One row per bounty submission (promoted from Airtable comment) | 200–2000 |
| 4 | Appeals |
One row per author-filed appeal against a triage decision | 10–50 |
| 5 | Arbitrations |
One row per hunter-filed dispute against a rejection (LLM arbitrator output) | 20–100 |
| 6 | Escrow Ledger |
Per-dollar release schedule for contributor escrow balances | 1000–10000 |
| 7 | Audit Log |
Immutable record of every state change on Findings / Contributors / Arbitrations | 10000+ |
The two new ones since file 16 are Escrow Ledger (already referenced)
and Audit Log (new — addresses the dispute-reconstruction gap).
- Primary keys are Airtable Autonumber unless stated;
Tasks.slugis the exception (single-line text, unique). - Linked records are bidirectional by default; the reverse name in the linked table is given in parentheses.
- Formulas use Airtable's formula language verbatim, monospace.
- Required means Airtable's "required" flag is set OR an Automation rejects rows without it.
- Default value is set via Airtable's Default Value field option unless stated.
- Permission column (
R/Wper role) is intentional. Captures the permissions matrix inline so the schema and the access-control plan don't drift. Roles:HHunter,AAuthor,TTriager,STSenior Triager,AdmAdmin.R= read,W= write,–= no access.
One row per shipped task. Created manually or by sync from tasks/<slug>/
on the repo.
| Field | Type | Required | Formula / link | H | A | T | ST | Adm |
|---|---|---|---|---|---|---|---|---|
slug |
Single line (Primary) | Yes | unique; matches tasks/<slug>/ |
R | R(own) | R | R | RW |
author |
Linked → Contributors | Yes | (reverse: tasks_authored) |
R | R(own) | R | R | RW |
track |
Single select | Yes | options: standard, frontier; default standard |
R | R(own) | R | R | RW |
shipped_at |
Date | Yes | bounty window opens | R | R(own) | R | R | RW |
bounty_status |
Single select | Yes | options: Open, Closed; default Open |
R | R(own) | R | R | RW |
findings |
Linked → Findings | – | (reverse: target_task) |
R | R(own) | R | R | R |
total_deductions_to_author |
Rollup | – | SUM({deduction_from_author}) from linked Findings where triage_status = Accepted, capped via formula: MIN(SUM({deduction_from_author}), 200) |
R | R(own) | R | R | R |
remaining_author_at_risk |
Formula | – | 250 - {total_deductions_to_author} |
R | R(own) | R | R | R |
notes |
Long text | No | admin notes | – | – | R | R | RW |
Hunters see all Tasks (need to choose what to hunt). Authors only see their own task rows. Triagers and Admin see everything.
One row per real-identity contributor. The system of record for reputation, escrow, and clawback debt.
| Field | Type | Required | Formula / link | H | A | T | ST | Adm |
|---|---|---|---|---|---|---|---|---|
contributor_id |
Autonumber (Primary) | – | – | R(self) | R(self) | R | R | RW |
email |
Yes | Mercor contributor record key | R(self) | R(self) | – | R | RW | |
display_name |
Single line | Yes | shown in UI | R | R | R | R | RW |
roles_active |
Multi-select | Yes | options: Hunter, Author, Triager, Senior Triager, Frontier Verifier, Appeal Reviewer |
R(self) | R(self) | R | R | RW |
quality_score |
Number | Yes | default 1.0, range 0.0–2.0 |
R(self) | R(self) | R | R | RW |
quality_score_history |
Linked → Audit Log | – | filtered to entity_type=Contributor rows for this contributor |
R(self) | R(self) | R | R | R |
escrow_balance |
Rollup | – | SUM({amount}) from linked Escrow Ledger rows where released_at IS NULL AND slashed_at IS NULL |
R(self) | R(self) | R | R | R |
escrow_locked |
Rollup | – | sum of bond_amount_locked from Findings where triage_status = Pending and hunter = this |
R(self) | R(self) | R | R | R |
escrow_available |
Formula | – | {escrow_balance} - {escrow_locked} |
R(self) | R(self) | R | R | R |
lifetime_payouts |
Rollup | – | SUM({payout_to_hunter}) from accepted Findings |
R(self) | R(self) | R | R | R |
lifetime_slashed |
Rollup | – | SUM({bond_amount_locked} * IF({bond_disposition} = "Slashed-100", 1, IF({bond_disposition} = "Slashed-50", 0.5, 0))) |
R(self) | R(self) | R | R | R |
clawback_debt |
Number | – | default 0; written by Automation on AI-slop/Malicious decisions |
R(self) | R(self) | R | R | RW |
ai_slop_warnings |
Number | – | default 0 |
R(self) | R(self) | R | R | RW |
submissions_filed |
Count | – | count of linked Findings (any status) | R(self) | R(self) | R | R | R |
submissions_accepted |
Count | – | count where triage_status = Accepted |
R(self) | R(self) | R | R | R |
accept_rate |
Formula | – | IF({submissions_filed} > 0, {submissions_accepted} / {submissions_filed}, BLANK()) |
R(self) | R(self) | R | R | R |
suspended_from |
Multi-select | – | role names; default empty | R(self) | R(self) | R | R | RW |
suspension_until |
Date | – | auto-lift date | R(self) | R(self) | R | R | RW |
triager_quality_metric |
Number | – | only populated for Triager role; default 1.0. Updated by arbitrator outcomes (see Automations) |
– | – | R(self) | R | RW |
created_at |
Created time | – | – | R(self) | R(self) | R | R | R |
Self-only visibility: for fields marked R(self), an Airtable
Interface filter restricts the view to the contributor's own row.
Hunters and Authors cannot see other contributors' escrow / reputation
/ debt.
The core operational table. One row per bounty submission.
Submission flow: hunter leaves a comment on a Tasks row → triager
promotes the comment into a Findings row by clicking "Promote to
Finding" on an Interface page (a Run-a-Script action under the hood
that pre-populates target_task, hunter, source_comment_url,
submitted_at). The triager then completes the structured fields and
makes a decision.
| Field | Type | Required | Formula / link | H | A | T | ST | Adm |
|---|---|---|---|---|---|---|---|---|
finding_id |
Autonumber (Primary) | – | – | R(own) | R(own task) | R | R | R |
target_task |
Linked → Tasks | Yes | (reverse: findings) |
R(own) | R(own task) | R | R | RW |
hunter |
Linked → Contributors | Yes | (reverse: findings_as_hunter) |
R(own) | R(own task) | R | R | RW |
source_comment_url |
URL | Yes | Airtable deep link to original comment | R(own) | R(own task) | R | R | RW |
submitted_at |
Created time | – | auto | R(own) | R(own task) | R | R | R |
claimed_tier |
Single select | Yes | Catastrophic, Critical, Major, Minor, Duplicate, Co-finder |
R(own) | R(own task) | R | R | RW |
failure_category |
Single select | Yes | Foil-pass, Contamination, Judge-prompt-leak, Likert-rescue, Calibration-instability, Tooling, Doc-fix, Ambiguity, Duplicate, Other |
R(own) | R(own task) | R | R | RW |
evidence_url |
URL | Yes | gist / commit / trial output | R(own) | R(own task) | R | R | RW |
reproduction_steps |
Long text | Yes (Catastrophic/Critical/Major) | – | R(own) | R(own task) | R | R | RW |
triage_status |
Single select | Yes | Pending, Accepted, Rejected, Duplicate-of, Needs-more-evidence; default Pending |
R(own) | R(own task) | R | R | RW |
triager |
Linked → Contributors | Auto on status change | (reverse: findings_triaged) |
R(own) | R(own task) | R | R | RW |
triaged_at |
Date | Auto on status change | written by Automation when status leaves Pending | R(own) | R(own task) | R | R | R |
actual_tier |
Single select | Yes if Accepted | same options as claimed_tier | R(own) | R(own task) | R | R | RW |
tier_changed_from_claimed |
Formula | – | {claimed_tier} != {actual_tier} |
R(own) | R(own task) | R | R | R |
bond_amount_locked |
Formula | – | SWITCH({claimed_tier}, "Catastrophic", 50, "Critical", 25, "Major", 10, "Duplicate", 5, "Minor", 2, "Co-finder", 5) |
R(own) | R(own task) | R | R | R |
bond_disposition |
Single select | – | Pending, Returned, Slashed-50, Slashed-100; default Pending |
R(own) | R(own task) | R | R | RW |
badness_tier |
Single select | Yes if Rejected | N/A, Honest-mistake, Error, Flagrant, AI-slop, Malicious; default N/A |
R(own) | R(own task) | R | R | RW |
senior_triager_cosign |
Linked → Contributors | Yes if badness_tier ∈ {Flagrant, AI-slop, Malicious} |
– | R(own) | R(own task) | R | R | RW |
payout_to_hunter |
Formula | – | IF({triage_status} = "Accepted", SWITCH({actual_tier}, "Catastrophic", 200, "Critical", 100, "Major", 50, "Duplicate", 50, "Minor", 0, "Co-finder", 25), 0) |
R(own) | R(own task) | R | R | R |
deduction_from_author |
Formula | – | IF({triage_status} = "Accepted", SWITCH({actual_tier}, "Catastrophic", 200, "Critical", 100, "Major", 50, "Duplicate", 25, "Minor", 0, "Co-finder", 25), 0) (Duplicate: 25 per author, two authors per Duplicate finding) |
R(own) | R(own task) | R | R | R |
clawback_amount |
Formula | – | IF({badness_tier} = "AI-slop", {bond_amount_locked}, IF({badness_tier} = "Malicious", BLANK(), 0)) — Malicious clawback is computed by Automation against 90-day rolling earnings |
R(own) | R(own task) | R | R | R |
auto_quality_signal |
Single select | – | clearly-ai, unclear, clearly-human; written by AI-slop pre-screen Automation |
– | – | R | R | RW |
auto_quality_confidence |
Number | – | 0.0–1.0 | – | – | R | R | RW |
templated_prose_score |
Number | – | 0.0–1.0 | – | – | R | R | RW |
fabricated_evidence_likelihood |
Number | – | 0.0–1.0 | – | – | R | R | RW |
high_risk_queue |
Formula | – | AND({auto_quality_signal} = "clearly-ai", {auto_quality_confidence} >= 0.9, {fabricated_evidence_likelihood} >= 0.7) |
– | – | R | R | R |
arbitration |
Linked → Arbitrations | – | (reverse: finding) |
R(own) | R(own task) | R | R | R |
appeal |
Linked → Appeals | – | (reverse: finding) |
R(own) | R(own task) | R | R | R |
bad_actor_signals |
Multi-select | – | options from file 12 | – | – | R | R | RW |
grace_applied |
Multi-select | – | options from file 12 | R(own) | R(own task) | R | R | RW |
triager_notes |
Long text | – | reasoning | R(own) | R(own task) | R | R | RW |
appeal_window_ends |
Formula | – | DATEADD({triaged_at}, 7, 'days') |
R(own) | R(own task) | R | R | R |
Author bad-faith hook: the deduction_from_author rollup feeds
back to Tasks.total_deductions_to_author, capped at $200/task per
file 01. A pattern of multiple shipped tasks where the author hit the
$200 cap quickly is a signal for senior-triager attention. Surfaced
via a Senior Triager view filtering Tasks by total_deductions_to_author >= 150.
Triager bad-faith hook: triager link + arbitrator-outcome
Automation decrements the triager's own triager_quality_metric on
OVERTURN (−1.0) and PARTIAL_OVERTURN (−0.5). A triager's metric
falling below 0.5 freezes their triage assignments pending senior
review. Implemented in Automations (file 20).
Filed by authors against Accepted decisions (not by hunters — hunter
disputes go through Arbitrations).
| Field | Type | Required | Formula / link | H | A | T | ST | Adm |
|---|---|---|---|---|---|---|---|---|
appeal_id |
Autonumber (Primary) | – | – | – | R(own) | R | R | RW |
finding |
Linked → Findings | Yes | (reverse: appeal) |
– | R(own) | R | R | RW |
appellant |
Linked → Contributors | Yes | must be finding.target_task.author |
– | R(own) | R | R | RW |
filed_at |
Created time | – | auto | – | R(own) | R | R | R |
appeal_basis |
Long text | Yes | new evidence / wrong tier / misclassification | – | R(own) | R | R | RW |
appeal_reviewer |
Linked → Contributors | Yes | Senior Triager; must differ from original finding.triager |
– | R(own) | R | R | RW |
outcome |
Single select | – | Pending, Upheld, Overturned, Partial; default Pending |
– | R(own) | R | R | RW |
outcome_notes |
Long text | – | reviewer's rationale | – | R(own) | R | R | RW |
decided_at |
Date | – | auto on outcome change |
– | R(own) | R | R | R |
LLM arbitrator output for hunter-filed disputes against rejections.
| Field | Type | Required | Formula / link | H | A | T | ST | Adm |
|---|---|---|---|---|---|---|---|---|
arbitration_id |
Autonumber (Primary) | – | – | R(own) | – | R | R | RW |
finding |
Linked → Findings | Yes | (reverse: arbitration) |
R(own) | – | R | R | RW |
disputer |
Linked → Contributors | Yes | must be finding.hunter |
R(own) | – | R | R | RW |
triggered_at |
Created time | – | auto | R(own) | – | R | R | R |
hunter_dispute_text |
Long text | Yes | – | R(own) | – | R | R | RW |
dispute_bond |
Number | – | default 20; locked from disputer's escrow on creation |
R(own) | – | R | R | R |
model_id |
Single line | – | written by arbitrator Automation; e.g., claude-opus-4-7 |
R(own) | – | R | R | R |
prompt_version_sha256 |
Single line | – | hash of rubrics/bounty-arbitration.toml at call time |
R(own) | – | R | R | R |
input_payload_sha256 |
Single line | – | hash of (finding payload, triager reason, dispute text, task snapshot) | R(own) | – | R | R | R |
output_decision |
Single select | – | UPHOLD, OVERTURN, PARTIAL_OVERTURN, UNCERTAIN, Pending |
R(own) | – | R | R | R |
output_rationale |
Long text | – | arbitrator's reasoning | R(own) | – | R | R | R |
output_revised_badness_tier |
Single select | – | set on PARTIAL_OVERTURN |
R(own) | – | R | R | R |
arbitration_cost_usd |
Number | – | ~$0.50 typical | – | – | R | R | RW |
senior_triager_panel |
Multi link → Contributors | – | set on UNCERTAIN |
R(own) | – | R | R | RW |
final_outcome |
Single select | – | mirrors output_decision or senior panel verdict |
R(own) | – | R | R | RW |
bond_disposition_dispute |
Formula | – | SWITCH({final_outcome}, "UPHOLD", "Slashed", "OVERTURN", "Returned", "PARTIAL_OVERTURN", "Returned", BLANK()) |
R(own) | – | R | R | R |
frivolous_dispute_quality_delta |
Formula | – | IF({final_outcome} = "UPHOLD", -0.05, 0) |
R(own) | – | R | R | R |
overturn_bonus_paid |
Formula | – | IF({final_outcome} = "OVERTURN", 25, 0) |
R(own) | – | R | R | R |
Per-dollar entries. Each entry tracks one inflow to a contributor's escrow and its release / slash status.
Why per-dollar (or per-batch) and not per-contributor: the 90-day release rule applies per-entry (when each dollar entered), not per- contributor. A single per-contributor escrow_balance would lose the release timing.
| Field | Type | Required | Formula / link | H | A | T | ST | Adm |
|---|---|---|---|---|---|---|---|---|
ledger_id |
Autonumber (Primary) | – | – | R(own contrib) | R(own contrib) | R | R | RW |
contributor |
Linked → Contributors | Yes | (reverse: escrow_entries) |
R(own contrib) | R(own contrib) | R | R | RW |
amount |
Number | Yes | USD | R(own contrib) | R(own contrib) | R | R | RW |
source |
Single select | Yes | Starter-grant, Payout-holdback, Bond-returned, Arbitrator-overturn-bonus |
R(own contrib) | R(own contrib) | R | R | RW |
source_finding |
Linked → Findings | – | set when source = Payout-holdback or Bond-returned |
R(own contrib) | R(own contrib) | R | R | RW |
source_arbitration |
Linked → Arbitrations | – | set when source = Arbitrator-overturn-bonus |
R(own contrib) | R(own contrib) | R | R | RW |
entered_escrow_at |
Created time | – | auto | R(own contrib) | R(own contrib) | R | R | R |
release_eligible_at |
Formula | – | DATEADD({entered_escrow_at}, 90, 'days') |
R(own contrib) | R(own contrib) | R | R | R |
released_at |
Date | – | written by release Automation | R(own contrib) | R(own contrib) | R | R | RW |
slashed_at |
Date | – | written by slash Automation | R(own contrib) | R(own contrib) | R | R | RW |
slash_finding |
Linked → Findings | – | the finding that caused the slash | R(own contrib) | R(own contrib) | R | R | RW |
slash_amount |
Number | – | actual dollars slashed (may be partial if last entry covers a remainder) | R(own contrib) | R(own contrib) | R | R | RW |
status |
Formula | – | IF({slashed_at}, "Slashed", IF({released_at}, "Released", IF({release_eligible_at} <= TODAY(), "Pending release", "Locked"))) |
R(own contrib) | R(own contrib) | R | R | R |
Immutable record of every state change on the four operational tables
(Findings, Contributors, Arbitrations, Escrow Ledger). Written
exclusively by Automations; never edited by humans.
Purpose: dispute reconstruction (a contributor claims their escrow was wrongly slashed → audit log shows exactly which Automation fired when, who triggered the source event), and triager-quality auditing (a senior triager investigating drift can see every classification a specific triager made and how the arbitrator ruled).
| Field | Type | Required | Formula / link | H | A | T | ST | Adm |
|---|---|---|---|---|---|---|---|---|
audit_id |
Autonumber (Primary) | – | – | – | – | – | R | R |
recorded_at |
Created time | – | auto | – | – | – | R | R |
entity_type |
Single select | Yes | Finding, Contributor, Arbitration, Escrow Ledger |
– | – | – | R | R |
entity_id |
Single line | Yes | the affected row's primary key | – | – | – | R | R |
field_changed |
Single line | Yes | name of the field | – | – | – | R | R |
old_value |
Long text | – | stringified prior value | – | – | – | R | R |
new_value |
Long text | – | stringified new value | – | – | – | R | R |
change_source |
Single select | Yes | Triager-action, Automation, Senior-override, Admin-correction |
– | – | – | R | R |
actor |
Linked → Contributors | – | for Triager-action and Senior-override; null for Automation |
– | – | – | R | R |
automation_name |
Single line | – | for Automation source |
– | – | – | R | R |
related_finding |
Linked → Findings | – | for any change traceable to a finding | – | – | – | R | R |
related_arbitration |
Linked → Arbitrations | – | for arbitrator-triggered changes | – | – | – | R | R |
notes |
Long text | – | for Admin-correction only, explain why |
– | – | – | R | RW |
Hunters and authors cannot see this table. Only Senior Triagers and Admin. Disputes that require an audit reference are routed through Senior Triager review.
Audit log entries cannot be edited or deleted (Airtable Base permission: this table is "Editor-locked"; only Owner can modify existing rows; only Automations can create new ones). This is enforced at the base-level permission setting, not in a formula.
Translating the per-column R/W into Airtable's actual permission model:
| Role | Base permission level | Per-table overrides | Per-row filter |
|---|---|---|---|
| Hunter | Read-only on base | Findings: editor on rows where hunter = self (for comment-promotion preview only — but the triager owns the promote); Arbitrations: editor on rows where disputer = self for the initial dispute text only |
hunter = currentUser() filter on Findings views; contributor_id = currentUser() filter on Contributors view; matching filter on Escrow Ledger |
| Author | Read-only on base | Appeals: editor on rows where appellant = self |
author = currentUser() filter on Tasks; downstream filters on Findings (target_task.author = currentUser()) |
| Triager | Editor on Findings, Arbitrations | Read-only on Contributors (no W on quality_score directly — Automations write it); read-only on Escrow Ledger |
None |
| Senior Triager | Editor everywhere except Audit Log (which is Editor-locked); can sign-off on Flagrant/AI-slop/Malicious; can override Triager decisions | – | None |
| Admin | Owner / Creator | Everything writable except Audit Log row deletion (which is impossible) | None |
The author/hunter row filters are implemented as Interface filters plus base-level "Limited record permissions" rules. Airtable's permission model supports both. Hunters and authors operate primarily through Interface pages (file 19), not direct base access — Interface permissions are the primary control, with base permissions as the backstop.
The system catches author bad-faith via three derived signals visible to Senior Triagers:
Tasks.total_deductions_to_authorrollup. A task hitting the $200 cap in < 7 days of shipping is a signal. Senior Triager view:Tasksfiltered by(TODAY() - shipped_at) <= 7 AND total_deductions_to_author >= 150.- Per-author rollup across tasks. Add a
Contributorsrollup:tasks_hit_deduction_cap= count of linkedtasks_authoredrows wheretotal_deductions_to_author >= 200. Two such tasks in 30 days triggers manual senior-triager review. - Collusion indicator. A
Contributorsformula fielddominant_hunter_pct= "what % of accepted bounty findings against this author's tasks come from a single hunter contributor". > 40% triggers manual review (the M3 lifetime cap from02-anti-collusion.md, applied manually via the surfaced view).
These are signal-surfacing fields, not auto-enforcement. The Senior Triager makes the call to suspend / clawback based on what the signal shows + investigation.
The Contributors.triager_quality_metric field is the primary
deterrent. Automations update it:
- On every Arbitration
final_outcome = OVERTURNagainst a triager's rejection: −1.0 to the triager's metric - On
PARTIAL_OVERTURN: −0.5 - On every Appeal
outcome = Overturned: −0.5
If triager_quality_metric < 0.5, an Automation sets the triager's
suspended_from = [Triager] and suspension_until = TODAY() + 30.
Senior Triager review required to restore.
Visibility: a Triager sees their own metric (R(self)) so they have feedback. Other Triagers and Senior Triagers see all metrics for calibration.
When standing up the base:
- Create Tables 1, 2 (Tasks, Contributors) first — they're the leaf nodes everything else links to.
- Create Table 6 (Escrow Ledger) — needed for Contributor rollups.
- Create Table 3 (Findings) — links to 1, 2, 6.
- Create Tables 4, 5 (Appeals, Arbitrations) — link to 3.
- Create Table 7 (Audit Log) — links to everything; Automations populate it.
- Configure base permissions per the matrix above.
- Create Automations per file 20.
- Create Views and Interfaces per files 18 and 19.
- Onboarding (KYC, contributor agreement signing, starter-escrow grant trigger). Mercor admin process, not in the Airtable.
- Task admission (how a shipped
tasks/<slug>/becomes aTasksrow). Manual or external sync, not in the Airtable. - Triager rotation / assignment (who picks up which Finding from the queue). Self-assign in v1; no schema needed.
- Tax / 1099 (out of scope per user direction).
- Public stats dashboard (deferred to v2 per file 14).
- All 7 tables build cleanly in a fresh Airtable base in one afternoon by following this spec.
- Field formulas all parse and evaluate correctly on test rows.
- Permissions matrix matches the R/W columns when tested with each role's actual account.
- Audit Log cannot be edited by any non-Owner role.
- Escrow Ledger rollups produce correct
escrow_balance,escrow_locked, andescrow_availablefor a contributor with a mix of released, locked, and slashed entries. - Triager and senior-triager views immediately surface author bad-faith signal rows and triager-quality drift.
One view per workflow surface, per role. Names use the format
<Role>: <View name> so they sort naturally in the Airtable view
sidebar.
Field column lists use the table-spec field names from 17-airtable-schema.md. Filter conditions use Airtable filter-builder syntax conceptually.
- Source: Tasks
- Filter:
bounty_status = "Open" - Sort:
shipped_atdesc - Group:
track(standard / frontier separated) - Fields shown:
slug,author.display_name,track,shipped_at,remaining_author_at_risk - Purpose: browse what's available to hunt
- Source: Findings
- Filter:
hunter = LOGGED_IN_USER() - Sort:
submitted_atdesc - Group:
triage_status - Fields shown:
finding_id,target_task.slug,claimed_tier,actual_tier,triage_status,badness_tier,bond_amount_locked,bond_disposition,payout_to_hunter,triaged_at,appeal_window_ends - Purpose: track own submissions through triage
- Source: Escrow Ledger
- Filter:
contributor = LOGGED_IN_USER() - Sort:
entered_escrow_atdesc - Group:
status(Locked / Pending release / Released / Slashed) - Fields shown:
ledger_id,amount,source,source_finding.finding_id,entered_escrow_at,release_eligible_at,status - Purpose: see escrow inflows / outflows + upcoming releases. Read-only.
- Source: Contributors
- Filter:
contributor_id = LOGGED_IN_USER() - Layout: single-record view (only one row will match)
- Fields shown:
display_name,quality_score,escrow_balance,escrow_locked,escrow_available,lifetime_payouts,lifetime_slashed,clawback_debt,ai_slop_warnings,submissions_filed,submissions_accepted,accept_rate,suspended_from,suspension_until - Purpose: self-status at a glance
- Source: Arbitrations
- Filter:
disputer = LOGGED_IN_USER() - Sort:
triggered_atdesc - Group:
final_outcome(Pending / UPHOLD / OVERTURN / PARTIAL_OVERTURN / UNCERTAIN) - Fields shown:
arbitration_id,finding.finding_id,triggered_at,dispute_bond,output_decision,final_outcome,bond_disposition_dispute,overturn_bonus_paid - Purpose: track dispute outcomes
- Source: Tasks
- Filter:
author = LOGGED_IN_USER() - Sort:
shipped_atdesc - Fields shown:
slug,track,bounty_status,shipped_at,total_deductions_to_author,remaining_author_at_risk - Purpose: monitor own tasks' bounty exposure
- Source: Findings
- Filter:
target_task.author = LOGGED_IN_USER() AND triage_status = "Pending" - Sort:
submitted_atdesc - Fields shown:
finding_id,target_task.slug,claimed_tier,failure_category,evidence_url,submitted_at - Visibility caveat: author sees the structured Finding fields but not the
triager,triager_notes,bad_actor_signals, orauto_quality_signal(those are triager-internal during Pending). Permissions in 17 §Permissions enforce this. - Purpose: awareness of what's being decided against own task. No action available until triage_status changes.
- Source: Findings
- Filter:
target_task.author = LOGGED_IN_USER() AND triage_status != "Pending" - Sort:
triaged_atdesc - Group:
triage_status - Fields shown:
finding_id,target_task.slug,claimed_tier,actual_tier,triage_status,badness_tier,deduction_from_author,triaged_at,triager_notes,appeal_window_ends - Purpose: see accepted findings against own tasks; basis for filing an Appeal
- Source: Appeals
- Filter:
appellant = LOGGED_IN_USER() - Sort:
filed_atdesc - Group:
outcome - Fields shown:
appeal_id,finding.finding_id,finding.target_task.slug,filed_at,appeal_basis,appeal_reviewer.display_name,outcome,outcome_notes,decided_at - Purpose: track appeal status
- Source: Contributors
- Filter:
contributor_id = LOGGED_IN_USER() - Layout: single-record view
- Fields shown: same as Hunter equivalent, scoped to author-relevant fields
- Purpose: self-status
- Source: Findings
- Filter:
triage_status = "Pending" AND high_risk_queue = FALSE - Sort:
submitted_atasc (oldest first — fairness) - Fields shown:
finding_id,target_task.slug,hunter.display_name,hunter.quality_score,claimed_tier,failure_category,evidence_url,reproduction_steps,auto_quality_signal,auto_quality_confidence,submitted_at,bond_amount_locked - Purpose: the main triage queue. AI-slop high-risk ones are siphoned off into the separate view below so the normal queue stays clean.
- Source: Findings
- Filter:
triage_status = "Pending" AND high_risk_queue = TRUE - Sort:
submitted_atasc - Fields shown: same as Pending Queue plus
templated_prose_score,fabricated_evidence_likelihood,hunter.ai_slop_warnings,hunter.lifetime_slashed - Purpose: AI-slop pre-screen positives that need a senior triager's attention. By Airtable Interface permissions this view is read-only for regular Triagers and editable only by Senior Triagers — the high-risk queue requires Senior co-sign anyway, so routing directly is cleaner than the regular Triager triaging and then asking for co-sign.
- Source: Findings
- Filter:
triager = LOGGED_IN_USER() AND triaged_at >= TODAY() - 30 - Sort:
triaged_atdesc - Group:
triage_statusthenbadness_tier - Fields shown:
finding_id,target_task.slug,hunter.display_name,claimed_tier,actual_tier,triage_status,badness_tier,triaged_at,appeal(link badge → if appealed, see outcome),arbitration(link badge → if disputed, see outcome) - Purpose: triager's own work; the appeal/arbitration link badges show which decisions got pushed back, useful for self-calibration
- Source: Contributors
- Filter:
contributor_id = LOGGED_IN_USER() AND "Triager" IN roles_active - Layout: single-record + Audit Log linked view filtered to
entity_type = Contributor AND entity_id = self AND field_changed = "triager_quality_metric" - Fields shown:
triager_quality_metric, plus the linked Audit Log entries showing every metric delta with the related Finding/Arbitration - Purpose: triager sees their own quality trajectory and what caused each delta
- Source: Findings
- Filter:
badness_tier IN ("Flagrant", "AI-slop", "Malicious") AND senior_triager_cosign IS NULL - Sort:
triaged_atasc - Fields shown:
finding_id,target_task.slug,triager.display_name,badness_tier,triager_notes,bond_amount_locked,hunter.display_name,hunter.quality_score - Visibility: read-only to regular Triagers (informational — they can see what's queued), editable to Senior Triagers (who do the co-sign)
- Purpose: Senior Triager workload queue
- Same as
Triager: Pending Cosign Requestsabove, but writable for Senior to setsenior_triager_cosign = self
- Source: Appeals
- Filter:
outcome = "Pending" - Sort:
filed_atasc - Fields shown:
appeal_id,finding.finding_id,finding.target_task.slug,finding.actual_tier,appellant.display_name,appellant.quality_score,filed_at,appeal_basis,finding.triager.display_name,finding.triager_notes - Auto-assignment hint: an Automation flags appeals where
appeal_reviewer = finding.triageras invalid (reviewer must differ); Senior Triager picks the reviewer from this view.
- Source: Arbitrations
- Filter:
output_decision = "UNCERTAIN" AND final_outcome = "Pending" - Sort:
triggered_atasc - Fields shown:
arbitration_id,finding.finding_id,finding.target_task.slug,disputer.display_name,hunter_dispute_text,output_rationale,senior_triager_panel - Purpose: arbitrator escalations; Senior panel decides
- Source: Contributors
- Filter:
"Triager" IN roles_active - Sort:
triager_quality_metricasc - Group: none (flat sorted list — worst-at-top draws attention)
- Fields shown:
display_name,triager_quality_metric,submissions_accepted(count of findings they triaged that were Accepted),suspended_from,suspension_until - Purpose: the canonical "are any triagers drifting" view. Anyone below 0.7 is on watch; below 0.5 is auto-suspended.
- Source: Tasks
- Filter:
total_deductions_to_author >= 150 AND (TODAY() - shipped_at) <= 14 - Sort:
total_deductions_to_authordesc - Fields shown:
slug,author.display_name,shipped_at,total_deductions_to_author,remaining_author_at_risk,findings(count badge) - Purpose: tasks that hit deduction cap fast → investigate
- Source: Contributors
- Filter:
"Author" IN roles_active AND dominant_hunter_pct >= 0.4 - Sort:
dominant_hunter_pctdesc - Fields shown:
display_name,dominant_hunter_pct,submissions_accepted, linkedtasks_authored - Purpose: the M3 lifetime-cap manual check. Above 40% triggers investigation.
- Source: Findings
- Filter:
triaged_at >= TODAY() - 7 - Sort:
triaged_atdesc - Group:
triager.display_name - Fields shown:
finding_id,target_task.slug,hunter.display_name,claimed_tier,actual_tier,triage_status,badness_tier,appeal.outcome,arbitration.final_outcome - Purpose: the M7-equivalent weekly 100% senior scan. Grouped by triager so calibration drift across triagers is visible.
- Source: Findings
- Filter:
target_task.track = "frontier" AND badness_tier IN ("Flagrant", "AI-slop", "Malicious") AND senior_triager_cosign IS NULL - Same shape as Cosign Queue but frontier-specific (frontier findings get extra scrutiny because the verifier hash is at stake)
- Source: Contributors
- Filter:
clawback_debt > 0 - Sort:
clawback_debtdesc - Fields shown:
display_name,email,clawback_debt,lifetime_payouts,suspended_from,suspension_until - Purpose: dollar amounts to deduct from next Mercor payments. Exported to Mercor's contributor-payment system on a cadence.
- Source: Contributors
- Filter:
suspended_from IS NOT EMPTY - Sort:
suspension_untilasc - Fields shown:
display_name,email,suspended_from,suspension_until,quality_score,clawback_debt - Purpose: active-suspension list; near-expiry rows show what's about to come back online
- Source: Audit Log
- Filter: none
- Sort:
recorded_atdesc - Group:
entity_typethenchange_source - Fields shown: all
- Purpose: dispute reconstruction. Filter on-demand by
related_findingoractorwhen investigating a specific case.
- Source: Escrow Ledger
- Filter: none
- Sort:
entered_escrow_atdesc - Group:
status - Fields shown: all
- Purpose: financial overview of all in-flight escrow
- Source: Arbitrations
- Filter: none
- Sort:
triggered_atdesc - Summary bar:
SUM({arbitration_cost_usd})by month - Fields shown:
arbitration_id,triggered_at,model_id,arbitration_cost_usd,final_outcome,bond_disposition_dispute - Purpose: monitor LLM arbitrator spend against the $25/quarter ceiling from file 16
Listed deliberately so future contributors don't add them:
- Public stats view. Deferred to v2 (M9 dashboard).
- Cross-role pseudonym tracking. No pseudonyms in v1.
- Throughput-allocation queue. No automated assignment in v1.
- Per-author hunter cooldown enforcement view. Manual M2 in v1 — Senior Triager checks during Collusion Watch.
Each view is bound to one or more Airtable Interface pages (file 19). Direct base access for non-Admin roles should be disabled — Hunters / Authors / Triagers / Senior Triagers work exclusively through Interface pages, which inherit the view filters and apply additional UI gating. Direct base access bypasses the per-row filters and shows raw data.
- Each view in this file exists in the Airtable base with the documented filter, sort, group, and field selection.
- A test Hunter account sees only their own Findings + Contributor record + Escrow Ledger entries via the Hunter views; they cannot see other Hunters' data.
- A test Author account sees only Findings against their own Tasks and cannot see triager-internal fields on Pending Findings.
- The Senior Triager
Weekly Decision Scanview, when filtered to a single triager, immediately surfaces all that triager's decisions in the week + the appeal/arbitration outcome on each. - The
Author Bad-Faith Signalview triggers a test row when a contrived task accumulates $150+ in deductions within 14 days. - The
Triager Quality Watchview sorts the worst triager to the top and includes the auto-suspension threshold (0.5) as a visible reference (via a Summary field or conditional color).
The actual UI for each role. Non-Admin roles work exclusively through Interfaces — direct base access is disabled.
Each page below has: who sees it, what views it surfaces (from 18-airtable-views.md), and an ASCII mockup.
Interface elements used: Header, Number cards, Record list (filtered
to a view), Record detail, Form, Button (running an Automation),
Filter bar.
┌──────────────────────────────────────────────────────────────────────────┐
│ Codebase Atlas Bounty [Alice Chen ▼] [Help] │
├──────────────────────────────────────────────────────────────────────────┤
│ │
│ Welcome back, Alice │
│ │
│ ┌─Quality─────────┐ ┌─Escrow──────────┐ ┌─Lifetime─────────────────┐ │
│ │ │ │ │ │ │ │
│ │ 1.45 │ │ $87 / $200 │ │ $1,240 paid │ │
│ │ (rank: ★★★) │ │ $25 locked │ │ 18 of 23 accepted (78%) │ │
│ │ │ │ $62 available │ │ 0 AI-slop warnings │ │
│ │ │ │ │ │ │ │
│ └─────────────────┘ └─────────────────┘ └──────────────────────────┘ │
│ │
│ ┌─My recent findings──────────────────────────────────────────────┐ │
│ │ #234 rustfs-iam-proxy Critical→Critical ✓ Accepted $100 │ │
│ │ #229 spacy-training Major→Major ✓ Accepted $50 │ │
│ │ #225 harbor-oauth Critical ⏳ Pending │ │
│ │ #221 rustfs-default Critical→Major ⚠ Tier-inflate $50 │ │
│ │ [See all my findings →] │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─Escrow releases coming up──────────────────────────────────────┐ │
│ │ $20 unlocks 2026-06-15 (from finding #229) │ │
│ │ $20 unlocks 2026-07-02 (from finding #234) │ │
│ │ $5 unlocks 2026-08-14 (from finding #225, conditional) │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ [Browse open tasks →] [My disputes] [My escrow log] │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────────┘
Surfaces: Hunter: My Contributor Record, Hunter: My Findings (top 5),
Hunter: My Escrow (Pending release subset).
┌──────────────────────────────────────────────────────────────────────────┐
│ Browse Open Tasks [← Home] │
├──────────────────────────────────────────────────────────────────────────┤
│ Filter: ◉ All ○ Standard ○ Frontier Sort: [Newest first ▼] │
├──────────────────────────────────────────────────────────────────────────┤
│ │
│ ╭─ rustfs-default-creds-startup-abort ────────────── standard ─╮ │
│ │ by Bob Lee · shipped 2026-05-24 (3d ago) │ │
│ │ Author at risk: $200 of $200 remaining │ │
│ │ Findings filed: 0 │ │
│ │ [Open task →] │ │
│ ╰───────────────────────────────────────────────────────────────╯ │
│ │
│ ╭─ spacy-training-state-flow ────────────────────── frontier ─╮ │
│ │ by Carol Wu · shipped 2026-05-22 (5d ago) │ │
│ │ Author at risk: $150 of $200 remaining │ │
│ │ Findings filed: 1 (1 accepted Major) │ │
│ │ [Open task →] │ │
│ ╰───────────────────────────────────────────────────────────────╯ │
│ │
│ ╭─ harbor-claude-oauth-auth ─────────────────────── standard ─╮ │
│ │ by Dan Park · shipped 2026-05-20 (7d ago) │ │
│ │ Author at risk: $200 of $200 remaining │ │
│ │ Findings filed: 2 (0 accepted, 2 pending) │ │
│ │ [Open task →] │ │
│ ╰───────────────────────────────────────────────────────────────╯ │
│ │
│ [Load more] │
└──────────────────────────────────────────────────────────────────────────┘
Surfaces: Hunter: Open Tasks view.
┌──────────────────────────────────────────────────────────────────────────┐
│ rustfs-default-creds-startup-abort [← Browse] │
├──────────────────────────────────────────────────────────────────────────┤
│ │
│ Track: standard · Author: Bob Lee · Shipped: 2026-05-24 │
│ Bounty status: Open · Author at risk: $200 of $200 │
│ │
│ Task materials │
│ ▸ README ▸ instruction.md ▸ solution/answer.txt│
│ ▸ Reward rubrics (4) ▸ Run an oracle locally │
│ │
│ Existing findings (check before submitting — duplicates pay 50%) │
│ ┌────────────────────────────────────────────────────────────────────┐ │
│ │ (none yet) │ │
│ └────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─ How to submit ──────────────────────────────────────────────────┐ │
│ │ 1. Click [Submit Finding] below │ │
│ │ 2. Leave a comment with: what's wrong, repro steps, evidence URL │ │
│ │ 3. Choose your claimed tier — your bond is locked from escrow: │ │
│ │ Catastrophic $50 · Critical $25 · Major $10 · Minor $2 │ │
│ │ 4. A triager reviews within 72hr typical │ │
│ │ │ │
│ │ Your available escrow: $62 → you can submit at any tier │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ [📝 Submit Finding] │
│ │
└──────────────────────────────────────────────────────────────────────────┘
The Submit Finding button opens a structured comment compose dialog
on the Task record. The comment becomes the source the triager
promotes to a Finding row.
┌──────────────────────────────────────────────────────────────────────────┐
│ Finding #221 rustfs-default-creds-startup-abort [← My Findings] │
├──────────────────────────────────────────────────────────────────────────┤
│ │
│ Status: ⚠ Accepted at lower tier (tier-inflation) │
│ Submitted: 2026-05-21 Triaged: 2026-05-23 Triager: Erin S. │
│ │
│ Claimed: Critical Actual: Major │
│ Failure category: Foil-pass │
│ Bond: $25 returned to escrow │
│ Payout: $50 (Major) │
│ Quality score impact: −0.05 (tier inflation) │
│ │
│ Triager notes: │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ The finding is real — the foil scores 0.42 instead of <0.4 — │ │
│ │ but the impact is contained to one task, not systemic, so this │ │
│ │ is a Major, not a Critical. Critical requires either │ │
│ │ cross-task impact or contamination evidence; neither was shown.│ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │
│ ╭─ Dispute window open until 2026-05-30 ──────────────────────╮ │
│ │ Think the triager got the tier wrong? │ │
│ │ File a dispute. Costs $20 from your escrow (locked during │ │
│ │ arbitration). Returned if the arbitrator overturns; │ │
│ │ slashed if the arbitrator upholds. │ │
│ │ [🗳 File Dispute] │ │
│ ╰──────────────────────────────────────────────────────────────╯ │
│ │
└──────────────────────────────────────────────────────────────────────────┘
For rejected findings the panel changes:
│ Status: ✗ Rejected (Flagrant error) │
│ Bond: $12.50 slashed, $12.50 returned │
│ Quality score impact: −0.10 │
For AI-slop:
│ Status: ✗ Rejected (AI-slop) │
│ Bond: $25 slashed │
│ Clawback: $25 owed from past 30 days of earnings │
│ Quality score impact: −0.20 │
┌──────────────────────────────────────────────────────────────────────────┐
│ Codebase Atlas — Author View [Bob Lee ▼] [Help] │
├──────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─Quality─────────┐ ┌─Tasks shipped───┐ ┌─Lifetime deductions────┐ │
│ │ 1.10 │ │ 8 │ │ $325 across 8 tasks │ │
│ │ ●●●○○ │ │ (1 frontier) │ │ Avg $41/task │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────────────┘ │
│ │
│ ┌─My tasks ────────────────────────────────────────────────────────┐ │
│ │ ⓘ rustfs-default-creds Open $0 deducted $250 at risk │ │
│ │ ⓘ harbor-oauth Open $50 deducted $200 at risk │ │
│ │ ⓘ spacy-training (frontier) Open $150 deducted $100 at risk │ │
│ │ ⓘ rustfs-iam-proxy Open $200 deducted $50 at risk │ │
│ │ ▸ ... (4 more closed) │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─Findings pending against my tasks (informational) ───────────────┐ │
│ │ #229 rustfs-iam-proxy Critical claimed filed 2d ago │ │
│ │ #231 harbor-oauth Major claimed filed 1d ago │ │
│ │ #233 rustfs-default Minor claimed filed 5h ago │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─Recently decided ────────────────────────────────────────────────┐ │
│ │ #227 spacy-training ✓ Accepted Major −$50 [Appeal until 6/1]│ │
│ │ #225 harbor-oauth ✓ Accepted Critical −$100 [Appeal until 5/30] │ │
│ │ #220 rustfs-default ✗ Rejected (no action needed) │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────────┘
Surfaces: Author: My Tasks, Author: Incoming Findings (Pending),
Author: Decided Findings.
The pending findings card is informational only — author cannot see the triager or triager notes until the triager decides. This preserves triage independence.
┌──────────────────────────────────────────────────────────────────────────┐
│ File Appeal · Finding #227 [← Author Home] │
├──────────────────────────────────────────────────────────────────────────┤
│ │
│ Appeal against: │
│ Task: spacy-training-state-flow │
│ Hunter: Alice Chen (quality 1.45) │
│ Triager: Erin Santos │
│ Decision: Accepted at Major ($50 deducted from you) │
│ Triaged: 2026-05-25 (2d ago) │
│ │
│ Triager's reasoning: │
│ "The trajectory drift between training states is observable when │
│ spaCy's pipeline serializes interim weights..." │
│ [Read full reasoning →] │
│ │
│ Your appeal basis (3-10 sentences, specific): │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │
│ Valid bases: new evidence not previously surfaced; demonstrably │
│ wrong tier classification; misapplication of a documented criterion. │
│ Not valid: dislike of the hunter; subjective disagreement. │
│ │
│ ⓘ Your appeal goes to an independent Senior Triager (not Erin). │
│ ⓘ If Overturned, the finding is reversed and your $50 returned. │
│ ⓘ Decisions typically within 7 days. │
│ │
│ [Cancel] [Submit Appeal] │
│ │
└──────────────────────────────────────────────────────────────────────────┘
The primary working surface. Three-pane layout:
┌──────────────────────────────────────────────────────────────────────────┐
│ Triager Workbench [Erin Santos · Q-metric 0.92] │
├─────────────────┬────────────────────────────────────────────────────────┤
│ QUEUES │ Finding #235 · harbor-oauth-auth │
│ │ │
│ Pending (12) │ Hunter: Alice Chen (Q 1.45 · 18/23 accepted) │
│ ● #235 5h ago │ Submitted: 2026-05-27 09:14 │
│ #234 7h ago │ Bond locked: $25 (Critical) │
│ #233 8h ago │ │
│ #232 11h ago │ Claimed: Critical · Failure: Judge-prompt-leak │
│ ... │ │
│ │ ┌─Comment thread (source) ─────────────────────────┐ │
│ High-risk (2) │ │ Alice: The grading prompt at rubrics/oauth-flow │ │
│ #218 2d ago │ │ leaks the answer when the agent quotes the env │ │
│ #210 4d ago │ │ var verbatim. See repro at gist.github.com/... │ │
│ │ └──────────────────────────────────────────────────┘ │
│ Cosigns (3) │ │
│ #209 senior │ Pre-screen signals │
│ #207 senior │ auto_quality_signal: clearly-human (0.91) │
│ #205 senior │ templated_prose_score: 0.12 │
│ │ fabricated_evidence_likelihood: 0.08 │
│ │ │
│ My week │ Hunter history │
│ 18 decided │ Recent 30d: 4 Accepted, 0 Rejected │
│ 0 overturned │ Lifetime slashed: $0 │
│ Q-metric 0.92 │ AI-slop warnings: 0 │
│ │ │
│ │ ─────────────── Decision ─────────────── │
│ │ ◉ Accept ○ Reject ○ Needs more evidence │
│ │ Actual tier: [Critical ▼] │
│ │ Payout: $100 to Alice Deduction: $100 from Dan │
│ │ Bond disposition: Returned to escrow │
│ │ │
│ │ Triager notes: ┌──────────────────────────────────┐ │
│ │ │ Reproduced cleanly. The leak is │ │
│ │ │ deterministic with the prompt as │ │
│ │ │ written. │ │
│ │ └──────────────────────────────────┘ │
│ │ │
│ │ [Save Draft] [Submit Decision] │
└─────────────────┴────────────────────────────────────────────────────────┘
Rejection mode swaps the lower panel:
│ │ ○ Accept ◉ Reject ○ Needs more evidence │
│ │ │
│ │ Badness tier: ◉ Honest-mistake ○ Error ○ Flagrant │
│ │ ○ AI-slop ○ Malicious │
│ │ │
│ │ ⓘ Flagrant/AI-slop/Malicious require Senior cosign │
│ │ before applying. Submitting will route to the │
│ │ Senior Cosign queue. │
│ │ │
│ │ Bond disposition: Returned to escrow (Honest-mistake) │
│ │ Quality delta: −0.025 │
│ │ │
│ │ Triager notes: [required for rejections] │
┌──────────────────────────────────────────────────────────────────────────┐
│ Promote Comment to Finding [← Workbench] │
├──────────────────────────────────────────────────────────────────────────┤
│ │
│ Pending comments (unpromoted) │
│ ╭──────────────────────────────────────────────────────────────────╮ │
│ │ Task: harbor-oauth-auth Alice · 5h ago │ │
│ │ "The grading prompt at rubrics/oauth-flow leaks..." │ │
│ │ [Promote →] │ │
│ ├──────────────────────────────────────────────────────────────────┤ │
│ │ Task: rustfs-default-creds Frank · 1d ago │ │
│ │ "I think this fails to handle the case where..." │ │
│ │ [Promote →] │ │
│ ├──────────────────────────────────────────────────────────────────┤ │
│ │ Task: spacy-training Greta · 2d ago │ │
│ │ "Bug: the answer doesn't account for X" │ │
│ │ [Promote →] │ │
│ ╰──────────────────────────────────────────────────────────────────╯ │
│ │
│ Promoting creates a Findings row pre-filled with: │
│ · target_task, hunter, source_comment_url, submitted_at │
│ You then fill in: claimed_tier, failure_category, evidence_url, │
│ reproduction_steps (from the comment content) before sending to │
│ the triage queue. │
│ │
│ Auto-extract: │
│ ☑ Pull URLs from comment as evidence_url candidates │
│ ☑ Detect claimed tier from comment keywords ("Critical", etc.) │
│ ☑ Run AI-slop pre-screen on promotion │
│ │
└──────────────────────────────────────────────────────────────────────────┘
Single landing page with three queues stacked:
┌──────────────────────────────────────────────────────────────────────────┐
│ Senior Triager Workbench [Hannah Park · ST role active] │
├──────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─Cosign queue (3) ────────────────────────────────────────────────┐ │
│ │ #209 · spacy-training Triager: Erin Badness: AI-slop │ │
│ │ Hunter Q: 0.85 Bond at stake: $25 [Review →] │ │
│ │ #207 · rustfs-default Triager: Erin Badness: Flagrant │ │
│ │ Hunter Q: 1.10 Bond at stake: $25 [Review →] │ │
│ │ #205 · harbor-oauth Triager: Frank Badness: Malicious│ │
│ │ Hunter Q: 0.60 Bond + clawback [Review →] │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─Appeal queue (2) ────────────────────────────────────────────────┐ │
│ │ #189-appeal · Bob (author) v Finding #227 filed 1d ago │ │
│ │ [Review →] │ │
│ │ #185-appeal · Carol (author) v Finding #220 filed 3d ago │ │
│ │ [Review →] │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─UNCERTAIN arbitrations (1) ──────────────────────────────────────┐ │
│ │ Arb #45 · Finding #210 Arbitrator: UNCERTAIN │ │
│ │ Disputer: Alice Panel size: 0/3 assigned [Assemble →] │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─Triager quality watch ───────────────────────────────────────────┐ │
│ │ ⚠ Frank Lopez Q-metric 0.62 ← approaching 0.5 threshold │ │
│ │ Erin Santos Q-metric 0.92 │ │
│ │ Ivan Wei Q-metric 1.18 │ │
│ │ [Full watch →] │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─Author bad-faith signal ─────────────────────────────────────────┐ │
│ │ ⚠ Task `rustfs-iam-proxy` by Bob — $200 deducted in 4 days │ │
│ │ Bob's lifetime cap-hits: 2 in 30 days [Investigate →] │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ [Weekly Decision Scan (this week: 87)] │
│ │
└──────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────┐
│ Cosign Request · Finding #209 [← Workbench] │
├──────────────────────────────────────────────────────────────────────────┤
│ │
│ Triager Erin classifies this as: AI-slop │
│ Consequence if confirmed: │
│ · Hunter Alice: $25 bond slashed │
│ · Clawback: $25 from past 30 days ($150 earned) → $25 owed │
│ · Quality score: 1.45 → 1.25 │
│ · AI-slop warnings: 0 → 1 │
│ │
│ Full finding context: │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ [Finding #209 detail — same layout as Triager Workbench right] │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │
│ Triager's reasoning: │
│ "Reproduction steps reference `pipeline.serialize()` which does │
│ not exist in spaCy 3.x — see grep output. Comment prose matches │
│ template patterns from finding #195 (same hunter, also rejected)." │
│ │
│ Pre-screen signals: │
│ auto_quality_signal: clearly-ai (0.93) │
│ fabricated_evidence_likelihood: 0.82 │
│ │
│ Senior decision: │
│ ◉ Confirm AI-slop ○ Downgrade to Flagrant ○ Downgrade to Error│
│ ○ Reject classification (Triager misjudged — escalate to investigation)│
│ │
│ Senior notes: [required] │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │
│ [Cancel] [Apply Decision] │
│ │
└──────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────┐
│ Admin Dashboard [Mercor Admin ▼] │
├──────────────────────────────────────────────────────────────────────────┤
│ │
│ This month │
│ ┌─Findings──┐ ┌─Payouts─────┐ ┌─Slashes───┐ ┌─Arbitrator spend─────┐ │
│ │ 142 │ │ $4,250 │ │ $187 │ │ $12.50 / $25 budget │ │
│ │ +18% MoM │ │ to 23 ctrs │ │ 11 events │ │ 20 arbitrations │ │
│ └───────────┘ └─────────────┘ └───────────┘ └──────────────────────┘ │
│ │
│ ┌─Clawback debts (active) ─────────────────────────────────────────┐ │
│ │ Dave Smith $87 AI-slop on #198 + #203 │ │
│ │ Greta Lin $25 AI-slop on #189 │ │
│ │ Mike Torres $310 Malicious on #178 (collusion w/ #176 auth)│ │
│ │ Total owed: $422 │ │
│ │ [Export to Mercor contributor payment system] │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─Suspended contributors ──────────────────────────────────────────┐ │
│ │ Dave Smith suspended from [Hunter] until 2026-09-12 │ │
│ │ Mike Torres suspended from [all roles] until 2027-05-15 │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─Operational signals ─────────────────────────────────────────────┐ │
│ │ Pending findings queue depth: 12 (target: <30) │ │
│ │ Avg time-to-triage (7d rolling): 38 hours (target: <72h) │ │
│ │ Senior cosign backlog: 3 (target: <10) │ │
│ │ Appeals open: 2 (target: <5) │ │
│ │ Arbitrations open: 1 (target: <5) │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ [Audit Log Search →] [Full Escrow Ledger →] [Weekly Scan →] │
│ │
└──────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────┐
│ Audit Log Search [← Dashboard] │
├──────────────────────────────────────────────────────────────────────────┤
│ │
│ Filter by: │
│ Entity: [Finding ▼] Entity ID: [#209 ] │
│ Date range: [2026-05-20] to [2026-05-27] │
│ Actor: [any contributor ▼] Source: [any ▼] │
│ [Search] │
│ │
│ Results: 14 entries │
│ ┌────────┬─────────────────────┬──────────────────┬────────────────┐ │
│ │ Time │ Field changed │ Old → New │ Source / Actor │ │
│ ├────────┼─────────────────────┼──────────────────┼────────────────┤ │
│ │ 5/27 │ created │ — │ Auto / promote │ │
│ │ 5/27 │ auto_quality_signal │ — → clearly-ai │ Auto / prescreen│ │
│ │ 5/27 │ high_risk_queue │ — → TRUE │ Auto / formula │ │
│ │ 5/27 │ triage_status │ Pending → Rejct │ Triager / Erin │ │
│ │ 5/27 │ badness_tier │ N/A → AI-slop │ Triager / Erin │ │
│ │ 5/27 │ senior_triager_... │ — → Hannah Park │ Senior / Hannah│ │
│ │ 5/27 │ bond_disposition │ Pending → Slshd-100│ Auto / bond │ │
│ │ 5/27 │ clawback_amount │ — → $25 │ Auto / clawbck │ │
│ │ ... │ (6 more) │ │ │ │
│ └────────┴─────────────────────┴──────────────────┴────────────────┘ │
│ │
│ [Export CSV] │
│ │
└──────────────────────────────────────────────────────────────────────────┘
| Interface page | Roles with access |
|---|---|
| Hunter Home | Hunter |
| Hunter: Browse Tasks | Hunter |
| Hunter: Task Detail | Hunter |
| Hunter: My Finding Detail | Hunter (own findings only) |
| Author Home | Author |
| Author: File Appeal | Author (own decided findings only) |
| Triager Workbench | Triager, Senior Triager |
| Triager: Promote Comment | Triager, Senior Triager |
| Senior Triager Workbench | Senior Triager |
| Senior: Cosign Detail | Senior Triager |
| Admin Dashboard | Admin |
| Admin: Audit Log Search | Admin, Senior Triager (read-only) |
A contributor holding multiple roles (e.g., both Hunter and Triager) sees both Home pages in their navigation, switchable from the top-right account menu.
- Onboarding flow — handled outside Airtable (per file 17 §What's NOT)
- Public stats dashboard — deferred to v2
- Frontier verifier attestation page — uses the same Triager Workbench
pattern; documented in
frontier_track_admission/02-human-verification-protocol.md - Self-audit script results — runs in the author's local environment; not surfaced in Airtable
- A test Hunter account loads Hunter Home and sees only their own data; cannot navigate to Author / Triager / Admin pages.
- The Triager Workbench three-pane layout works for actual triage — triager can read a comment, make a decision, and submit without switching pages.
- The decision form gates: rejecting with
badness_tier ∈ {Flagrant, AI-slop, Malicious}routes the finding to the Senior Cosign queue and does not apply slashes/clawback until cosigned. - The Audit Log Search returns all 14 events for a fixture finding in the example.
- A Hunter with insufficient escrow cannot submit at the tier they want — the Task Detail page disables the higher tiers and shows "insufficient escrow" on the disabled options.
- An Author cannot see triager_notes or auto_quality_signal on Pending findings (verified by inspecting the Author: Incoming Findings card on Author Home).
This file is the build spec for every Automation and formula in the v1 Airtable base. Field references use 17-airtable-schema.md names.
Consolidated here for one-place lookup. Field-by-field these also appear in file 17; the verbatim formula text is below.
## bond_amount_locked
SWITCH(
{claimed_tier},
"Catastrophic", 50,
"Critical", 25,
"Major", 10,
"Duplicate", 5,
"Minor", 2,
"Co-finder", 5,
0
)
## payout_to_hunter
IF(
{triage_status} = "Accepted",
SWITCH(
{actual_tier},
"Catastrophic", 200,
"Critical", 100,
"Major", 50,
"Duplicate", 50,
"Minor", 0,
"Co-finder", 25,
0
),
0
)
## deduction_from_author
## (Duplicate: 25 per author; the *other* author is debited via the
## second Finding linked to the same root issue)
IF(
{triage_status} = "Accepted",
SWITCH(
{actual_tier},
"Catastrophic", 200,
"Critical", 100,
"Major", 50,
"Duplicate", 25,
"Minor", 0,
"Co-finder", 25,
0
),
0
)
## tier_changed_from_claimed
{claimed_tier} != {actual_tier}
## clawback_amount
## Malicious is left BLANK and computed by Automation against
## 90-day earnings rollup; AI-slop is exactly the slashed bond.
IF(
{badness_tier} = "AI-slop", {bond_amount_locked},
IF(
{badness_tier} = "Malicious", BLANK(),
0
)
)
## high_risk_queue
AND(
{auto_quality_signal} = "clearly-ai",
{auto_quality_confidence} >= 0.9,
{fabricated_evidence_likelihood} >= 0.7
)
## appeal_window_ends
DATEADD({triaged_at}, 7, 'days')
## total_deductions_to_author
## (rollup with conditional; field is a Rollup field
## pointing to linked Findings filtered to Accepted)
SUM(values) # base
## Then in the rollup conditions: WHERE {triage_status} = "Accepted"
## Apply cap separately via:
## total_deductions_capped (NEW formula field)
MIN({total_deductions_to_author}, 200)
## remaining_author_at_risk
250 - {total_deductions_capped}
## escrow_balance (Rollup from Escrow Ledger)
## Source: Escrow Ledger rows where status IN ("Locked", "Pending release")
## Aggregation: SUM({amount})
## escrow_locked (Rollup from Findings)
## Source: Findings where triage_status = "Pending" AND hunter = self
## Aggregation: SUM({bond_amount_locked})
## Plus disputer bond from Arbitrations where final_outcome IS NULL:
## add a second rollup `dispute_bonds_locked` from Arbitrations
## summing {dispute_bond} where {final_outcome} = "Pending"
## escrow_locked is then a formula: {finding_bonds_locked} + {dispute_bonds_locked}
## escrow_available
{escrow_balance} - {escrow_locked}
## accept_rate
IF({submissions_filed} > 0, {submissions_accepted} / {submissions_filed}, BLANK())
## dominant_hunter_pct (Author-side collusion signal)
## Computed via a separate scheduled Automation (file below); not pure formula.
## Falls back to a cached number field.
## tasks_hit_deduction_cap (Author-side)
## Rollup from linked tasks_authored:
## COUNTIF(values, {total_deductions_capped} = 200)
## bond_disposition_dispute
SWITCH(
{final_outcome},
"UPHOLD", "Slashed",
"OVERTURN", "Returned",
"PARTIAL_OVERTURN", "Returned",
"UNCERTAIN", BLANK(), # determined post-panel
BLANK()
)
## frivolous_dispute_quality_delta
IF({final_outcome} = "UPHOLD", -0.05, 0)
## overturn_bonus_paid
IF({final_outcome} = "OVERTURN", 25, 0)
## release_eligible_at
DATEADD({entered_escrow_at}, 90, 'days')
## status
IF({slashed_at}, "Slashed",
IF({released_at}, "Released",
IF({release_eligible_at} <= TODAY(), "Pending release",
"Locked"
)
)
)
The exact deltas applied by Automations. Hunter side:
| Trigger | Delta |
|---|---|
| Finding accepted at Catastrophic | +0.10 |
| Finding accepted at Critical | +0.05 |
| Finding accepted at Major | +0.025 |
| Finding accepted at Duplicate | +0.025 |
| Finding accepted at Minor | +0.01 |
| Finding accepted at Co-finder | +0.0125 (half of Major) |
| Tier inflation (accepted at lower than claimed) | −0.05 (in addition to the positive accrual for the accepted tier) |
| Rejected — Honest mistake | −0.025 |
| Rejected — Error | −0.05 |
| Rejected — Flagrant | −0.10 |
| Rejected — AI-slop | −0.20 |
| Rejected — Malicious | −0.50 |
| Appeal Overturned against your finding (you're the hunter, finding gets reversed) | −0.075 |
| Arbitration UPHOLD (your dispute was frivolous) | −0.05 |
| Arbitrator overturn bonus | +0.025 (positive: a valid dispute was vindicated) |
Triager side (triager_quality_metric):
| Trigger | Delta |
|---|---|
| Arbitrator OVERTURN against your rejection | −1.0 |
| Arbitrator PARTIAL_OVERTURN against your rejection | −0.5 |
| Appeal Overturned against your acceptance | −0.5 |
| Appeal Partial against your acceptance | −0.25 |
| Senior Triager rejects your badness_tier classification entirely on cosign review | −0.5 |
| 30 consecutive accepted/upheld decisions | +0.1 (slow recovery) |
- Trigger: Record created in Findings
- Conditions: none
- Actions:
- Run script: lock bond. Sum hunter's current escrow_balance via
Escrow Ledger rollup. If
escrow_available < bond_amount_locked→ delete the row + post error comment to source comment URL: "Insufficient escrow for claimed tier — submit at lower tier or wait for escrow release." Halt. - Run script (Haiku call): AI-slop pre-screen
- Input: comment text + reproduction_steps + evidence_url contents
- Prompt:
rubrics/bounty-ai-slop-prescreen.toml - Model:
claude-haiku-4-5 - Write:
auto_quality_signal,auto_quality_confidence,templated_prose_score,fabricated_evidence_likelihood - Cost: ~$0.005/call
- Audit Log entry:
change_source = "Automation",automation_name = "A1-finding-created",field_changed = "created"
- Run script: lock bond. Sum hunter's current escrow_balance via
Escrow Ledger rollup. If
- Trigger: Findings.triage_status changed from "Pending" to anything else
- Conditions: none
- Actions:
- Set
triaged_at = NOW(),triager = currentUser() - Conditional branch: if
triage_status = "Accepted":- Apply hunter quality_score delta per actual_tier (from table above)
- If
tier_changed_from_claimed = TRUE: additional −0.05 delta - Set
bond_disposition = "Returned". Create Escrow Ledger row: contributor=hunter, amount=bond_amount_locked, source="Bond-returned", source_finding=this. - Create Escrow Ledger row: contributor=hunter, amount=ROUND(payout_to_hunter * 0.20), source="Payout-holdback", source_finding=this. (20% holdback into escrow.)
- Trigger external payout (Mercor payment system webhook): pay hunter
payout_to_hunter * 0.80immediately. - Trigger author deduction: add
deduction_from_authorto author's clawback_debt OR offset against their next scheduled payment (logic external to Airtable).
- Conditional branch: if
triage_status = "Rejected":- Require
badness_tier != "N/A"(validation: if N/A, halt + flag triager) - If
badness_tier IN ("Flagrant", "AI-slop", "Malicious"): requiresenior_triager_cosign != NULL. If null, halt without applying slash/clawback; route to Senior Cosign Queue (file 19). The slash/clawback applies under A3 below when cosign lands. - If
badness_tier IN ("Honest-mistake", "Error"): apply delta from quality-score table; setbond_disposition = "Returned". Create Escrow Ledger row: bond returned (same as Accepted branch).
- Require
- Conditional branch: if
triage_status IN ("Duplicate-of", "Needs-more-evidence"):- Bond returned (no slash); no quality delta. Treats as informational.
- Audit Log entry for every field change.
- Set
- Trigger: Findings.senior_triager_cosign changed from NULL to a contributor link
- Conditions:
badness_tier IN ("Flagrant", "AI-slop", "Malicious") - Actions:
- Apply hunter quality_score delta per badness_tier
- Slash:
- Flagrant: slash 50% of bond_amount_locked
- AI-slop or Malicious: slash 100%
- Slash means: walk Escrow Ledger rows for hunter ordered by entered_escrow_at desc (newest first — least time-vested); for each, set
slashed_at = NOW(),slash_finding = this,slash_amount = MIN(remaining_to_slash, row.amount)until remaining_to_slash = 0.
- Clawback debt:
- AI-slop: compute past-30d earnings (rollup over hunter's accepted findings where triaged_at >= TODAY()-30); set hunter.clawback_debt += MIN(bond_amount_locked, past_30d_earnings)
- Malicious: compute past-90d earnings; set hunter.clawback_debt += past_90d_earnings (uncapped). Set hunter.suspended_from = ["Hunter", "Author", "Triager", "Senior Triager", "Frontier Verifier", "Appeal Reviewer"], hunter.suspension_until = TODAY() + 365
- AI-slop warnings increment: for AI-slop only, hunter.ai_slop_warnings += 1
- Audit Log: detailed entries for every change
- Trigger: Record created in Appeals
- Conditions: none
- Actions:
- Validate:
appellant = finding.target_task.author(halt + delete if mismatched) - Validate:
finding.triage_status = "Accepted"andTODAY() <= finding.appeal_window_ends(halt if outside window) - Validate:
appeal_reviewer != finding.triager(if equal, halt + prompt for different reviewer) - Audit Log: record creation
- Validate:
- Trigger: Appeals.outcome changed from "Pending" to anything else
- Actions:
- Set
decided_at = NOW() - Branch: if
outcome = "Overturned":- Reverse the finding: set
finding.triage_status = "Rejected",finding.badness_tier = "Error"(default for reversed acceptances),finding.actual_tier = BLANK(). - Reverse hunter accrual: subtract the original accept delta from hunter.quality_score; add −0.075 (Appeal overturned penalty).
- Slash hunter escrow by the original payout amount (clawback the payout) — same walking-ledger logic as A3.
- Refund author: subtract original deduction_from_author from author.clawback_debt (or credit next payment).
- Triager quality delta: triager.triager_quality_metric −= 0.5
- Reverse the finding: set
- Branch: if
outcome = "Partial":- Revise finding.actual_tier downward (Senior Triager fills in the revised tier in outcome_notes; Automation parses or it's set manually on the Finding record).
- Recompute payout / deduction at new tier.
- Net out the difference: pay back the diff from hunter, credit author the diff.
- Triager quality delta: −0.25
- Branch: if
outcome = "Upheld": no changes; the decision stands. - Audit Log: all changes
- Set
- Trigger: Record created in Arbitrations
- Actions:
- Validate:
disputer = finding.hunter,finding.triage_status = "Rejected",TODAY() <= finding.appeal_window_ends(yes, same window — file 16) - Validate: disputer.escrow_available >= 20 (halt + post error if insufficient)
- Lock dispute_bond: set dispute_bond = 20 (already defaulted)
- Run script: arbitrator call
- Build payload: finding payload, triager's reason, hunter's dispute text, task snapshot
- Hash with SHA-256 → input_payload_sha256
- Call Claude API: model
claude-opus-4-7, prompt fromrubrics/bounty-arbitration.toml, prompt_version_sha256 set from prompt file - Parse output → output_decision, output_rationale, output_revised_badness_tier
- Record arbitration_cost_usd from API response
- Run within 24 hours of trigger (Airtable Automations limit: schedule via cron job if synchronous call would exceed Automation timeout)
- Audit Log entry
- Validate:
- Trigger: Arbitrations.output_decision changed from NULL to a value
- Conditions: none
- Actions:
- Branch:
output_decision = "UPHOLD":- Set
final_outcome = "UPHOLD" - Slash dispute_bond from disputer escrow (same walking-ledger logic; $20)
- Apply disputer.quality_score −= 0.05 (frivolous_dispute_quality_delta)
- Set
- Branch:
output_decision = "OVERTURN":- Set
final_outcome = "OVERTURN" - Return dispute_bond (no Escrow Ledger change — bond was held, not slashed)
- Reverse finding: set
triage_status = "Accepted",badness_tier = "N/A",actual_tier = claimed_tier. Trigger A2 follow-on paths (payout, holdback escrow, etc.) — Airtable nested-automation pattern - Pay disputer overturn bonus $25 from slashed-bond pool (Mercor ops): create Escrow Ledger row, source="Arbitrator-overturn-bonus", amount=25
- Disputer quality_score += 0.025
- Triager metric: finding.triager.triager_quality_metric −= 1.0
- Set
- Branch:
output_decision = "PARTIAL_OVERTURN":- Set
final_outcome = "PARTIAL_OVERTURN" - Return dispute_bond
- Revise finding.badness_tier to output_revised_badness_tier
- Re-apply A3 with the new (lower) badness_tier
- Triager metric: −0.5
- Set
- Branch:
output_decision = "UNCERTAIN":- Set
final_outcome = "Pending"(will be set by A8 when senior panel decides) - Notify Senior Triagers via Airtable Slack/email integration
- Set
- Audit Log
- Branch:
- Trigger: Arbitrations.final_outcome changed from "Pending" (after output_decision = "UNCERTAIN")
- Actions: same as A7 branches based on final_outcome; senior panel's decision overrides arbitrator's UNCERTAIN
-
Trigger: Scheduled (every day at 00:00 UTC)
-
Actions: for each Escrow Ledger row where:
release_eligible_at <= TODAY()released_at IS NULLslashed_at IS NULL- AND contributor has no Findings with
triage_status = "Pending"(block release while a decision is in flight) - AND contributor has no Arbitrations with
final_outcome = "Pending"
Set
released_at = TODAY(). Trigger Mercor payment webhook for{amount}. Audit Log entry.
- Trigger: Scheduled (daily, 01:00 UTC)
- Actions:
- For each Contributor with "Triager" in roles_active:
- If
triager_quality_metric < 0.5AND"Triager" NOT IN suspended_from: setsuspended_from += "Triager",suspension_until = TODAY() + 30. Notify Senior Triagers.
- If
- For each Contributor with "Hunter" in roles_active:
- If
quality_score < 0.5AND"Hunter" NOT IN suspended_from: setsuspended_from += "Hunter",suspension_until = TODAY() + 180. Notify hunter. - If
quality_score >= 0.5AND"Hunter" IN suspended_fromANDsuspension_until <= TODAY(): remove "Hunter" from suspended_from. Notify.
- If
- For each Contributor with "Author" in roles_active:
- Compute
dominant_hunter_pct: of accepted Findings where target_task.author = this contributor, the % from a single hunter (across all hunters that have submitted ≥1 accepted finding against this author). Write to cached field. - If
dominant_hunter_pct >= 0.40: surface onSenior: Author Collusion Watch.
- Compute
- Audit Log entry for each suspension action.
- For each Contributor with "Triager" in roles_active:
Triggered by the relevant state changes. Each notification sends an email (Airtable Email Notification action) and optionally a Slack DM (if Slack workspace integration is configured).
| Event | Notify | Message |
|---|---|---|
| Findings.triage_status → "Accepted" | hunter | "Your finding #X was accepted at . $ paid (less 20% escrow holdback)." |
| Findings.triage_status → "Rejected" | hunter | "Your finding #X was rejected (<badness_tier>). . You have 7 days to dispute." |
| Findings.triage_status → "Accepted" | author | "A finding against your task was accepted at . $ deducted. You have 7 days to appeal." |
| Findings.senior_triager_cosign NULL → set | hunter | "Your finding #X has been classified as <badness_tier> by senior review. <slash/clawback summary>." |
| Findings.high_risk_queue → TRUE | hunter | "Your finding #X was routed to enhanced review (AI-content signals). Triage may take longer." |
| Appeals.outcome → "Overturned" | hunter, author | "Appeal on finding #X was overturned. Details." |
| Arbitrations.final_outcome → set | disputer | "Arbitration decision on your dispute: . Details." |
| Escrow Ledger row.released_at set | contributor | "$ from escrow released to your next payment." |
| Contributors.suspended_from changed | contributor | "Your access to role is suspended until . Reason: ." |
This is implemented as part of every other Automation rather than a standalone one — every action in A1–A11 ends with an Audit Log write. The standalone presence here is a reminder that the Audit Log is non-optional.
For changes that happen outside Automations (e.g., a Triager manually edits a field), Airtable's per-field "On record updated" trigger fires an Audit Log write. The trigger condition is a field-by-field list of "watched" fields per table (the operational fields, not formula fields).
The notifications in A11 respect the per-row visibility filters from 18 §Permissions binding:
- Hunters get notifications about their own findings only
- Authors get notifications about findings against their own tasks only
- Senior Triagers get notifications about cosign requests, UNCERTAIN arbitrations, and quality-watch alerts
- Admin gets a daily digest of all financial events
A notification never contains data the recipient couldn't see by logging in to the Airtable Interface.
The Automations call out to external systems at three points. These are not Airtable-native; they need glue scripts:
- Mercor payment webhook (A2, A5, A7, A9) — when payouts / clawback / overturn bonuses fire, push a row to the Mercor contributor payment system's existing intake.
- Claude API (A1 pre-screen, A6 arbitrator) — call from an Airtable Run Script action. Cost-tracked in arbitration_cost_usd field (arbitrator) or in a separate Admin monthly rollup (pre-screen).
- Slack notifications (A11, optional) — Airtable Slack integration if the Mercor workspace has it configured.
Estimated total external call volume in steady state:
- 200 pre-screens/month × $0.005 = $1
- 30 arbitrator calls/month × $0.50 = $15
- ~250 webhook payment events/month = free
- ~500 notification dispatches/month = free
Total monthly ops cost from Automations: ~$16. Well within the $50 budget from file 16.
Each Automation should have a paired fixture in
docs/plans/bounty_economics_v1/automation_fixtures/ (this directory
to be created when the automations are implemented). Each fixture is
a JSON payload representing the trigger event and the expected
post-state.
Minimum coverage:
- A1: insufficient-escrow rejection; successful pre-screen
- A2: Accept happy path; Reject Honest-mistake; Reject Flagrant (routes to A3)
- A3: Flagrant cosign applies 50% slash; AI-slop cosign applies 100% slash + clawback; Malicious applies suspension
- A4: invalid appellant rejected; valid appeal accepted
- A5: Overturned reverses payouts; Partial revises tier
- A6: insufficient escrow blocks dispute; valid dispute records hash
- A7: UPHOLD slashes bond; OVERTURN reverses + pays bonus; PARTIAL revises badness
- A9: release skipped when pending findings exist; release proceeds when clear
- A10: triager auto-suspension at < 0.5; hunter auto-restore at ≥ 0.5 after suspension expires
- Every Automation in A1–A11 implemented in the Airtable base.
- Every formula in the catalog parses and evaluates correctly on test rows.
- The 9 minimum-coverage fixtures pass end-to-end.
- Total monthly external-call cost stays under $50 in normal volume.
- Audit Log captures every state change documented in the per- Automation actions; no Automation skips its Audit Log write.
- Insufficient escrow on a hunter submission produces a clear error comment on the source Task row within 60 seconds of the comment being promoted.
- A pre-screen-flagged AI-slop submission is auto-routed to the High-Risk Queue before any Triager sees it in the regular Pending Queue.
- A triager with
triager_quality_metricdriven below 0.5 by arbitrator overturns is auto-suspended within 24 hours of crossing the threshold.
When you're done, all of the following must work end to end:
- Schema validates — every field, type, formula, rollup, link, and permission matches §C exactly.
- Views render — each of the 18 views from §D exists with the right filter/sort/group/columns.
- Interfaces work — the 10 pages from §E exist; per-role permissions gate correctly.
- Automations fire — A1–A12 plus the two new webhook receivers (A-admit + A-window-close) all trigger on synthetic events.
- Webhook end-to-end — synthetic
event=admitPOSTed to the webhook URL creates aTasksrow + books author fee. Subsequentevent=window_closeflips status to Closed and reconciles. Idempotency works on duplicate POSTs. - Smoke test on a real admission — coordinate with the repo owner to run admission against
tasks/hello-codebase-qa; confirm the webhook arrives,Tasksrow appears, bounty window opens.
- The GitHub-side POSTing code (
scripts/post_admission_webhook.py+.github/workflows/window_close.yml) — already shipped in the source repo. You just receive what it sends. - Mercor finance integration internals — append to the existing intake; don't build it.
- Mercor staff onboarding flow — Airtable Auth handles it.
After each major phase, reply with: ✅ done | ⏳ in progress | ❌ blocker + Airtable error. If a step is impossible (Airtable limitation), document and propose a workaround inline; don't skip silently.
Originating files in github.com/Kurry/codebase-atlas-samples (private):
docs/plans/bounty_economics_v1/17-airtable-schema.md→ §Cdocs/plans/bounty_economics_v1/18-airtable-views.md→ §Ddocs/plans/bounty_economics_v1/19-airtable-interfaces.md→ §Edocs/plans/bounty_economics_v1/20-airtable-automations.md→ §Fdocs/plans/admission_gate.md→ sender side of the webhooksdocs/plans/airtable_build_prompt.md→ repo-internal version of this handoff (with cross-refs instead of inlined content)
This standalone file is the agent-readable artifact; regenerate from sources whenever a spec changes.