Auditor’s Guide

For nerds, auditors, and the professionally suspicious. The deep version of how it works. Everything here is checkable; where it isn’t yet, it says so. Live addresses and backing status: trust & verification.

1 · What Is On-Chain, Exactly

One Scrypto component (guild-marketplace-escrow, Radix mainnet) enforces a six-state task lifecycle. The app (Next.js + Postgres) is a mirror and a convenience — the ledger is the source of truth, and a reconciler heals the database from chain events, never the reverse.

Open ──claim──▶ Claimed ──submit──▶ Submitted ──approve──▶ Released   (terminal)
  │                │                    │
  cancel        expire/cancel        dispute ──resolve/auto──▶ Released | Refunded (terminal)
  ▼                ▼
Refunded (terminal)
TransitionCallerAuthTime gate
create_taskposter— (funds reward + insurance atomically)—
claim_taskworkerworker badge Proof + claim bond; asserts worker != poster — an honest-mistake guard, not a self-dealing defence: poster is a caller-supplied parameter of create_task, so a decoy poster address bypasses it for the cost of gassets submit deadline (7d human / 1d agent)
cancel_taskpostertask-receipt Proof (not burned)Open only; credits reward + insurance to the poster's entitlement — moves nothing to the caller
cancel_task_by_poster_after_claimpostertask-receipt Proof (not burned)Claimed only; credits reward + insurance to the poster and the claim bond back to the worker's entitlement
expire_claimanyonePUBLICafter submit deadline + grace window (1h deployed); reopens the task and returns a bounty (expire_bounty_pct of the forfeited bond) to the caller, the rest to the owner's forfeited-bond vault
submit_taskworkerclaim-receipt burn (one-shot) + evidence hash committed—
approve_and_releasepostertask-receipt Proof (resource + local-id match). NOT burned: under pull the task receipt is a PERSISTENT entitlement key, retired separately via burn_task_receipt once both lanes are zerocredits per-party entitlements inside the component; moves nothing to the caller
release_after_review_timeoutanyonePUBLICSubmitted only, once review_deadline (submitted_at + review_window_secs, pinned at submit; 72h deployed) has passed; credits exactly what approve_and_release credits. Races raise_dispute first-to-commit
raise_disputeposter or workerreceipt/badge Proof + evidence hash—
resolve_disputearbiterarbiter badgeruling: PayWorker / RefundPoster / Split
auto_resolve_disputeanyonePUBLICafter the dispute window pinned at raise_dispute (7 days on the live component since the 2026-10-11 owner change; 72h before it)
withdraw_workerworkerworker (or agent) badge Proof — asserts the badge is the SAME non-fungible local id that claimed the taskdeposits BOTH lanes (reward + bond) into task.worker_account, PINNED AT CLAIM — the caller cannot name the destination
withdraw_posterpostertask-receipt Proof (persistent, not one-shot)deposits BOTH lanes into task.poster, PINNED AT create_task — the caller cannot name the destination
push_entitlementanyonePUBLIC — no Proof, no destination argumentdeposits a party's settled entitlement into the account PINNED for that party, through the same deposit path as withdraw_*; the caller pays the fee and receives nothing
withdraw_forfeited_bondsownerOWNER badgereturns the forfeited-bond vault for one resource to the caller; never touches a task's reward or insurance
burn_task_receiptpostertask-receipt Bucket (this one IS burned)TWO guards, both required: both poster entitlement lanes read zero, AND the task is terminal (Released | Refunded) — the second exists because on an Open task both lanes are legitimately zero, and burning there would strand the whole funded reward

Auth pattern worth auditing, and settlement is the part to read twice — it is two-phase. approve_and_release, auto_resolve_dispute and the cancel paths move no money outward: they drain the escrow vaults into per-party entitlements held inside the component. Three methods do return a Bucket to their caller, and none of them can reach a task's reward: resolve_dispute returns the arbiter's fee (taken from the insurance, capped per task by max_arbiter_fee_pct, callable only with the arbiter badge), expire_claim returns a bounty of expire_bounty_pct of the forfeited claim bond to whoever calls it once the claim deadline and its grace window (expire_grace_secs) have passed, and the OWNER-only withdraw_forfeited_bonds returns the rest of forfeited bonds. Entitlements leave only through withdraw_worker / withdraw_poster and the public push_entitlement, and each deposits from inside the component into an account pinned earlier — the worker's at claim_task, the poster's at create_task — because deposit_both_lanes calls try_deposit_or_abort on that stored address. The caller never names that destination, so those three being callable by the payee or by anyone moves no value to whoever calls them. That pin is the property worth attacking: the worker badge and the task receipt are both transferable bearer instruments (neither resource calls withdraw_roles!, so both inherit the SDK default withdrawer=AllowAll), so stealing one lets you CALL the withdrawal — and the money still lands in the pinned account. Public-mint governs forging a credential, transferability governs stealing one, and the payee pin is what makes the second one not pay. On receipts, the two differ: the claim receipt is one-shot and burned at submit_task, but the task receipt is NOT burned at approval — under pull it is a persistent entitlement key presented as a Proof, retired explicitly via burn_task_receipt once both poster lanes read zero and the task is terminal. Recurring acts use Proofs; safety releases (expire_claim, auto_resolve_dispute, release_after_review_timeout) are PUBLIC and time-gated. push_entitlement takes no Proof and no destination: anyone may call it to deliver a settled entitlement to its pin, so a lost or unnoticed credential does not strand the money (it still aborts if the pinned account refuses the deposit), and a hostile caller only pays the network fee to pay a stranger their own funds. One consequence worth auditing: deposit_both_lanes coerces the payee to an Account on every path that pays out, push_entitlement included, so a component (a funding pool, say) cannot yet be a payee.

2 · The Trust Claims, and How to Check Each One

1. “The owner badge cannot withdraw a task's reward, insurance or live claim bond.”
The owner badge has no authority over task vaults: it cannot touch reward or insurance. Everything it can do, all fifteen methods: manage the accepted-token whitelist (add_accepted_token, remove_accepted_token) and freeze or unfreeze a token (freeze_token, unfreeze_token), which only changes what NEW tasks may be funded in; withdraw forfeited claim bonds (withdraw_forfeited_bonds); and change twelve settings through ten calls. Eight of those calls are pinned into each task at the step that uses them, so a change reaches only tasks that get there afterwards: set_min_insurance_fraction and set_max_arbiter_fee_pct at funding; set_claim_bond_params, set_human_submit_deadline_secs and set_agent_submit_deadline_secs at claim; set_review_window_secs at submission; set_dispute_auto_resolve_secs and set_dispute_auto_resolve_default when a dispute is raised. Two are read at the moment a claim is expired, so a change DOES reach claims already in flight: set_expire_grace_secs (no upper bound, so the grace after a missed deadline could be cut to zero) and set_expire_bounty_pct (0–100% of the forfeited bond paid to whoever calls expire_claim; the rest goes to the owner's vault). Every one of these calls emits a public on-chain event. The arbiter badge sits in the same operator wallet as the owner badge: on a disputed task, its ruling decides how the reward, the insurance and the claim bond split between poster and worker, and it can pay no one else beyond the arbiter fee set on the task (0 on tasks funded through this app). Both badges are trusted roles today, and the aim is to hand the admin badge to the Radix DAO once the DAO is formed, with no date set.
Check: Read the blueprint's enable_method_auth! block (escrow/scrypto/guild-marketplace-escrow/src/lib.rs in github.com/radixguild/guild): every restrict_to: [OWNER] method is named above. Then watch the component's events on the Gateway for the matching *Updated / Token* / ForfeitedBondsWithdrawn events, and read expire_claim to see it use the live grace and bounty values.
2. “Nobody can be stranded.”
Claimed tasks expire publicly; disputes auto-resolve publicly after the window; submitted work can be released by anyone once the review window lapses, if the poster never acts. The winner finalizes from their own wallet — our keeper is watch-only by decision: it alerts humans and sends no transactions on the money path.
Check: Check it on the ledger: no keeper account signs on the money path — every approval and every withdrawal in the component's history is signed by the poster's or the worker's own account. Then observe that auto_resolve_dispute, expire_claim and release_after_review_timeout carry no badge requirement.
3. “The terms you saw are the terms that settle.”
Title, description, and structured terms (acceptance criteria, deadlines, revisions) are canonicalized and SHA-256 committed into the task at funding (work_brief_hash); submission evidence is likewise hashed. Disputes are judged against the committed brief — chat doesn't count unless it amended the brief.
Check: Recompute the canonical form (frozen v1/v2 formats in src/lib/escrow-utils.ts) against the on-chain hash via the Gateway.
4. “The mirror can't lie for long.”
App state is advanced only by verified on-chain events (single confirm path), and a keyless reconciler replays chain events on a cron.
Check: src/lib/escrow-confirm.ts — one writer, event-verified, idempotent ledger (UNIQUE(taskId, txType)).

3 · Economics (Deployed → Planned)

Deployed today

Claim bond 10% of reward (floor 76.45 XRD; the percentage, floor and cap are all owner settings); dispute window 7 days (an owner setting, raised from 72h on 2026-10-11; each dispute keeps the window in force when it was raised), default SplitEvenly; arbiter fee 0 (cap 10%). Ground truth per component address — see trust & verification. Insurance is not in that list: the component's min_insurance_fraction is 0 — an owner dial, not a floor you can rely on — and the 5% locked on every task here is this app's policy, applied when it builds the funding manifest.

Designed, not yet scheduled (first decided 2026-06-12)

Insurance becomes optional dispute coverage (~10–15% suggested, refunded if unused; no coverage ⇒ no dispute path — pure optimistic mode); SplitEvenly on dispute abandonment + reputation marks; mutually-signed splits (settle_by_agreement).

Two corrections (2026-08-06). This paragraph read “heartbeat removed in favor of deadline + mutual extension”. The second half was never true: heartbeat is removed outright — one claim deadline, fixed at claim time, no paid extensions and no extension method at all. Auto-release was refused for four months on a faucet objection (2026-08-04): submit_task validated nothing on its own, so a timed release without more would have paid out garbage submissions. The live blueprint (deployed 2026-09-13) answered that, not by changing submit_task’s validation, but by what now surrounds it: a non-zero evidence commitment bound to the committed brief, disputes live so a poster’s remedy is real, and the claim bond held to settlement so a garbage submission stakes real value once a dispute is raised. On that basis release_after_review_timeout is deployed and PUBLIC today. settle_by_agreement is still deferred. This is now the deployed reality — the live component has no heartbeat leg.

4 · Honest Gaps (the Register)

ElementStatusBest we have
Per-criterion enforced payoutsNo shipped precedent, anywhereChecklist-as-evidence routes to: full release / revision / mutual split / insured arbitration
Co-funder voting on acceptanceEvery attempt died or went unusedCurator-pattern pools (named acceptor, self-claim refunds, escrow-level timers protect the worker)
Subjective quality judgmentUnsolvable in generalCommitted brief + insured human arbiter + (planned) AI advisory opinion
Agent work verificationStandards in flux industry-wideOn-demand PR checks against the committed brief, plus the claim bond and escrow deadlines; the claim receipt is burned at submit, and the claiming badge is re-checked at withdraw. But the badge is a public mint that identifies no one, and of the recall-revocable agent badge this model assumes, three have been minted and two burned: the one that exists is held by the Guild's own worker agent, and none has ever been presented on a claim — so no human currently answers for an agent.

If you have a better mechanism for any of these: post it as a task. That is not a slogan; it is the product working on itself.

5 · Identity and Backing (Pseudonymous, With Receipts)

The operator is pseudonymous (bigdev / @bigdevxrd) with a verifiable on-chain track record. In lieu of doxxing, two backing steps: publishing the escrow blueprint source (done: it is public under Apache-2.0) with reproducible-build verification (planned), and a bug bounty paid through the Guild’s own escrow (planned; on-chain, visible). Status of each: trust & verification. Trustee-verified identity for tasks/projects over $50k USD is planned, not available yet: a named third party would attest the operator’s identity and standing without public disclosure. No trustee is retained.

6 · Known Limits — Read Before Relying

  • —Blueprint upgrades are migrations (a new component and an env swap), never in-place: the code behind a component address does not change. Its settings can — the owner badge can move the parameters listed under the owner's powers above. Most are pinned into a task at the step that uses them, so a change reaches only tasks that get there afterwards; the two expiry settings apply to claims already in flight. Read the live values per component address, not from this page.
  • —There is one arbiter badge today (supply 1; the operator can mint more) and the operator holds it. The deployed blueprint has no multi-arbiter rule to switch on: a panel with recall and assignment is a next-blueprint design, not a setting.
  • —The app and the escrow's Scrypto source are public (github.com/radixguild/guild, Apache-2.0): the task-escrow and NftSwap blueprints, and a deprecated earlier escrow package kept for reference. Reproducible-build verification, which would tie that source to the deployed packages, is still planned.
  • —This is experimental software on mainnet. The honest-gaps register above is live, not historical.