# Recourse — independent outcome contracts for AI work Recourse is independent outcome metering for AI work. A vendor and a buyer sign one outcome definition, and the units delivered against it are counted by a party neither side employs — then issued as a statement, hashed and signed, that either party can verify without asking Recourse. Terms are canonicalized and hashed, every state change is an immutable signed event in a hash-chained log, and funds stay on the parties' own rails: Recourse is never in the flow of funds. ## What runs today Running: parties and agent keys, terms and the terms hash, the signed hash-chained record, and holds on a manual rail. NOT running: verification, disputes, rulings, appeals, and party records. Those are specified in the Rulebook below and are not implemented yet. A commitment funded today is not checked against its acceptance criteria, and no dispute can be filed or ruled on it. The manual rail records that a hold was instructed; it does not move money and cannot enforce a release. Recourse is the third party in the deal and never holds the money. No Recourse-owned balance ever holds a party's funds. This document is the whole protocol in one fetch. It is generated from the Rulebook and the Outcome Standards, so it cannot drift from them. Website: https://recourse-mu.vercel.app. API: https://api.recourse.dev. Rulebook 0.4.0, effective 2026-08-26, hash sha256:8ffd8536fc49575c3b14601b5ac5bf4ce9ddfdc4c70ea145fee1c29cfa480a0f. ## 1. Parties A party is an organization or an agent, symmetrically: one record with keys, a record of past outcomes, and obligations. An agent is not a second-class caller and needs no separate integration. Credential families: - rk_test_… Organization key, sandbox. No money moves. - rk_live_… Organization key, live. - ak_… Agent key, bound to an agent party with a principal organization. Same endpoints, same scopes. Send it as `Authorization: Bearer `. Scopes: - commitments:write: Create, propose, accept, fund, and cancel commitments. - outcomes:write: Submit outcomes and evidence against an active commitment. - disputes:write: Open a dispute, post a bond, and file evidence. - records:read: Read a party's record and export an audit bundle. A key without the scope gets 403 insufficient_scope — never a partial result. Getting a party and a key: POST /v1/parties Create a party — an organization or an agent. Both are the same record with keys, a record, and obligations. Actor: Any credential SDK: client.request({ method: "POST", path: "/v1/parties", body }) POST /v1/parties/{id}/keys Mint a scoped key for that party. The token is returned once and never again. Actor: The party's principal SDK: client.request({ method: "POST", path: `/v1/parties/${id}/keys`, body }) The token is returned once, at creation, and never again. Only its SHA-256 is stored. ## 2. Discovery Everything an agent needs to start, without being told anything first: - https://recourse-mu.vercel.app/llms.txt This site in one short document: what Recourse is and where everything else is. - https://recourse-mu.vercel.app/llms-full.txt The whole protocol in one fetch: steps, states, standards, reason codes, errors, endpoints. - https://api.recourse.dev/.well-known/recourse.json Machine discovery: the API base, the scopes, the rulebook version, and the standards in force. - https://api.recourse.dev/.well-known/recourse-keys.json The Ed25519 public keys events and record snapshots are signed with. Verify without asking Recourse. - https://api.recourse.dev/openapi.json OpenAPI 3.1, generated from the live route table. Every client is generated from it. ## 3. The five steps The same sequence runs for a one-shot deliverable and for a stream of unit outcomes. Each step writes an immutable, signed event; the commitment's history is the source of truth and the tables are projections of it. 1. Terms are hashed and signed. The vendor writes terms against a public Outcome Standard. Recourse canonicalizes them (RFC 8785), takes the SHA-256, and the buyer signs that hash. Neither side can change what was agreed without changing the hash. 2. Funds are held on the parties' own rails. The buyer funds a hold on the rail it already uses, and no Recourse balance ever holds a party's money. On the manual rail that runs today, Recourse records that the hold was instructed; it does not move money and cannot enforce a release. A rail that can hold and release under Recourse's instruction lands with the Stripe adapter. 3. Verification against the terms. Each unit will be checked against the standard's acceptance criteria: deterministic checks first, a rubric model second, a person third, with every tier recording what it saw and why. The verification engine is not built yet, so nothing submitted today is checked. 4. Rulings with a reason code. Either party will be able to dispute inside the window and post a bond, and Recourse will apply the Rulebook to the evidence and issue a ruling citing a rule id and a reason code, appealable to a panel. The adjudication engine is not built yet, so no dispute can be filed or ruled on today. 5. The outcome is written to both records. Verified, rejected, disputed, and ruled units will write to both parties' records, and records are never edited. When a commitment closes, Recourse will seal it and sign the summary. The records engine is not built yet, so no record or seal is written today. ## 4. The five calls These five run a commitment from nothing to a signed, verifiable record. No other call is required. POST /v1/commitments [Idempotency-Key required] Create the draft and record its terms. Answers with the commitment id and the terms hash. Actor: Either party SDK: client.commitments.create(body) POST /v1/commitments/{id}/propose Publish the terms. Answers with the hosted contract link and the published terms hash. Actor: Vendor SDK: client.commitments.propose(id) POST /v1/commitments/{id}/accept Sign the terms hash. A hash that is not the current one is refused, not recorded. Actor: Buyer SDK: client.commitments.accept(id, { terms_hash }) POST /v1/commitments/{id}/fund [Idempotency-Key required] Instruct the hold on the rail and activate once it covers the amount the terms require. Actor: Buyer SDK: client.commitments.fund(id, { amount }) GET /v1/commitments/{id}/events Read the append-only, hash-chained log with its chain verification and the log public key. Actor: Either party SDK: client.commitments.events(id) Everything else the API answers: GET /v1/whoami The organization, agent party, scopes, and key id this credential resolves to. Actor: Any credential SDK: client.request({ method: "GET", path: "/v1/whoami" }) GET /v1/commitments The commitments this credential is a party to, newest first, cursor-paginated. Actor: Either party SDK: client.commitments.list({ role, state }) GET /v1/commitments/{id} One commitment: state, terms hash, timestamps, and the hold. Actor: Either party SDK: client.commitments.get(id) POST /v1/commitments/{id}/cancel Cancel while no hold is funded. After funding, the commitment settles through the protocol instead. Actor: Either party SDK: client.commitments.cancel(id) GET /v1/public/contracts/{slug} The hosted contract a buyer opens: the commitment, the full terms, and both parties. Actor: No credential SDK: client.contracts.get(slug) GET /v1/standards The Outcome Standards this deployment serves, with their ids, versions, and hashes. Actor: Any credential SDK: client.request({ method: "GET", path: "/v1/standards" }) GET /v1/reason-codes Every reason code a ruling may cite, with the rule text, for one rulebook version. Actor: Any credential SDK: client.request({ method: "GET", path: "/v1/reason-codes" }) A POST that creates or moves money requires an `Idempotency-Key` header. Repeating the same key with the same body replays the first answer; the same key with a different body is refused with idempotency_key_reused. A GET may be retried freely; a POST without a key must not be. In TypeScript: ```ts // 1 — Create. The terms above are canonicalized (RFC 8785) and hashed. This // call creates a commitment, so the SDK sends an Idempotency-Key: a retry // returns the same commitment instead of a second one. const commitment = await vendor.commitments.create({ kind: "outcome_stream", vendor_party_id: vendorPartyId, buyer_party_id: buyerPartyId, terms, }); // 2 — Propose. The vendor publishes the terms and gets the link the buyer opens. const { hosted_url, terms_hash } = await vendor.commitments.propose(commitment.id); // 3 — Accept. The buyer signs that exact hash. A hash that is not the current // one is refused with terms_hash_mismatch, not recorded. await buyer.commitments.accept(commitment.id, { terms_hash }); // 4 — Fund. The hold is instructed on the rail. The commitment activates once // the hold covers the amount the terms require. const funded = await buyer.commitments.fund(commitment.id, { amount: { amount_cents: 500_000, currency: "usd" }, }); funded.state; // "active" // 5 — Read the record. The chain check and the log public key come with it, so // the caller can verify the stream without trusting this response. const stream = await vendor.commitments.events(commitment.id); stream.verification; // { ok: true } stream.log_public_key; // raw Ed25519 public key, base64 ``` ## 5. Terms Terms are a typed JSON document. `terms_hash = sha256(JCS(terms))` in lowercase hex, rendered on the wire as `sha256:`. The buyer signs that hash, so neither side can change what was agreed without changing the hash. Two kinds: `outcome_stream` (a standard, a unit price, a budget per period) and `deliverable` (one deliverable, acceptance criteria, a deadline, a price). Money is always `{ amount_cents, currency: "usd" }` — integer cents, never a float. A worked example. This document hashes to the value below; canonicalize it and check. ```json { "kind": "outcome_stream", "standard": { "id": "OS-1", "version": "0.4.0" }, "unit_price": { "amount_cents": 175, "currency": "usd" }, "budget_per_period": { "amount_cents": 500000, "currency": "usd" }, "period": "monthly", "term_periods": 1, "evidence_source": { "type": "connector", "connector": "zendesk" }, "dispute": { "buyer_review_window_hours": 120, "statement_dispute_window_hours": 120, "evidence_window_hours": 48, "appeal_window_hours": 72, "dispute_bond": { "pct": 2, "min_cents": 2500, "max_cents": 250000 }, "appeal_bond": { "pct": 5, "min_cents": 10000, "max_cents": 1000000 }, "path": [ "rulebook", "panel", "aaa" ], "governing_law": "US-DE" }, "rulebook_version": "0.4.0" } ``` terms_hash = sha256:aef01ff030c41a3d18633bf4493f305d823be7624573c9b5b97465974ce2dfdd ## 6. Commitment states The commitment machine. A command that is not legal from the current state is refused with 409 invalid_transition; it is never silently ignored. - draft: Terms are on file. Nothing is published and no money has moved. - proposed: The terms are published at the hosted contract link. The buyer has not signed. - accepted: The buyer signed the terms hash. No hold exists yet. - funded: A hold exists on the rail. It does not yet cover the amount the terms require. - active: The hold covers the required amount. Outcomes count from here. - verifying: Evidence is being checked against the standard: deterministic criteria, then the rubric. - review: A rubric criterion could not be settled with confidence. A person decides, within 24 hours. - verified: The unit met the standard and is releasable. - rejected: A required criterion failed. The unit is not paid, and a reason code says which criterion. - disputed: A party disputed inside the window and posted a bond. The evidence window is open. - ruled: Recourse applied the Rulebook and issued a ruling citing a rule id and a reason code. - appealed: The losing party appealed to a panel and posted the appeal bond. - released: Funds moved to the vendor on the rail. The platform fee is taken here and nowhere else. - refunded: Funds returned to the buyer on the rail. - sealed: The commitment is closed and its summary is signed. Nothing further is written. - expired: The terms were not accepted before they lapsed. No money moved. - cancelled: A party cancelled before any hold was funded. - amending: New terms are being agreed. The terms in force still govern until the new hash is signed. ## 7. Outcome Standards A standard is a file, versioned and public. A commitment cites the standard id and version it was accepted under, and that version governs it for its whole life. Verification runs in one order: integrity, then deterministic criteria, then the source-of-truth cross-check, then rubric criteria, then the verdict. ### OS-1 Support Resolution (v0.4.0) Verify that a vendor's agent resolved a customer conversation without human handoff, so the vendor is paid per resolution and the buyer pays only for resolutions that hold. Unit: One customer conversation resolved by the vendor's agent without human handoff. Dedupe key: conversation_id Standard hash: sha256:06c0dd79f36093e9d90cbde1a7f0ae192267b57e6bac189ea6bee07befb75590 Windows: statement_dispute_hours=120h, evidence_hours=48h, inactivity_hours=72h Acceptance criteria: - c1 (deterministic, required): No human handoff Check: {"==":[{"var":"fields.handoff"},false]} - c2 (deterministic, required): Closure marker present Check: {"or":[{"!=":[{"var":"fields.closed_at"},null]},{"!=":[{"var":"fields.inactivity_marker_at"},null]},{"==":[{"var":"connector.customer_confirmed_close"},true]}]} - c3 (deterministic, required): Conversation id unique within the commitment Check: {"==":[{"var":"connector.recourse.dedupe_matches"},0]} - c4 (deterministic, material): Opened within the period or the 7 days before it Check: {"within_window":[{"var":"fields.opened_at"},{"var":"time.period_lookback_start"},{"var":"time.period_end"}]} - c5 (deterministic, material): No reopening within 7 days of close Check: {"==":[{"var":"connector.reopened_within_7d"},false]} - r1 (rubric, required): The agent addressed the customer's stated issue Question: Did the agent address the customer's stated issue, rather than merely close the conversation? Passes when: The final agent message responds to the issue the customer raised and does not defer it. Evidence: transcript - r2 (rubric, material): No policy violation evident Question: Does the transcript show the agent making a promise the vendor is not authorized to make? Passes when: No promise the vendor is not authorized to make appears in the transcript. Evidence: transcript Evidence artifacts: transcript (required, text/plain | text/markdown | application/json, max 10485760 bytes) Evidence fields, required: conversation_id, channel, opened_at, last_agent_message_at, handoff Evidence fields, optional: closed_at, inactivity_marker_at, customer_reply_after_close Exclusions: Spam conversations. Test conversations. Conversations the customer abandoned before the agent's first substantive reply. Gaming defenses: Dedupe on conversation_id. Reopen check against the source of truth. Bursts of outcomes at period end raise sampling. Handoff detection cross-checked against the buyer's system when connected. A required criterion that fails rejects the unit. Read the full standard at https://recourse-mu.vercel.app/standards#OS-1. ### OS-2 Qualified Lead (v0.4.0) Verify that a lead delivered by the vendor meets the buyer's stated qualification profile and is contactable, so the buyer pays only for leads it can act on. Unit: One lead delivered that meets the buyer's stated qualification profile and is contactable. Dedupe key: lead_id Standard hash: sha256:4c01e45abaadb00dc4477c7afdf9a5fc32182d2cb5f8552830f695926902de6c Windows: statement_dispute_hours=120h, evidence_hours=48h, contact_verification_max_age_hours=168h, dedupe_lookback_hours=4320h Acceptance criteria: - c1 (deterministic, required): Contact passes deliverability verification Check: {"or":[{"==":[{"var":"connector.email_check.deliverable"},true]},{"==":[{"var":"connector.phone_check.valid"},true]}]} - c2 (deterministic, required): Profile match on stated hard filters Check: {"and":[{">=":[{"var":"fields.employee_count"},{"var":"terms.profile.employee_count_min"}]},{"<=":[{"var":"fields.employee_count"},{"var":"terms.profile.employee_count_max"}]},{"in":[{"var":"fields.region"},{"var":"terms.profile.regions"}]},{"or":[{"==":[{"var":"terms.profile.titles"},null]},{"in":[{"var":"fields.title"},{"var":"terms.profile.titles"}]}]}]} - c3 (deterministic, required): Not a duplicate within 180 days Check: {"==":[{"var":"connector.recourse.dedupe_matches_180d"},0]} - c4 (deterministic, material): Company domain not on the buyer's suppression list Check: {"==":[{"var":"connector.suppression.domain_match"},false]} - c5 (deterministic, material): Company domain not affiliated with the vendor Check: {"==":[{"var":"connector.affiliation.vendor_match"},false]} - r1 (rubric, material): Title matches the target persona Question: Does the lead's title reasonably match the target persona described in the terms? Passes when: The title falls within the persona description in the terms, allowing for common variants of the same role. Evidence: title, company_domain Evidence artifacts: intent_signal (optional, application/json | text/plain, max 1048576 bytes) Evidence fields, required: lead_id, company_domain, title, employee_count, region, source, delivered_at, contact_verification, contact_verified_at Evidence fields, optional: none Exclusions: Generic mailboxes (info@, sales@) unless terms allow. Leads sourced from the buyer's own CRM. Gaming defenses: Vendor-affiliation detection. Contact verification must be recent (7 days old or less). Rate anomaly review. A required criterion that fails rejects the unit. Read the full standard at https://recourse-mu.vercel.app/standards#OS-2. ### OS-3 Booked Meeting, Held (v0.4.0) Verify that a meeting booked by the vendor's agent with an external prospect was attended by the buyer's representative and lasted long enough to count, so the buyer pays only for meetings that held. Unit: One meeting booked by the vendor's agent with an external prospect that the buyer's representative attended and that lasted at least the minimum duration. Dedupe key: meeting_id Standard hash: sha256:35866ddd94b84b8316e3757e1afa62c7cd6ed1a30307e3d1a6ee99a3d0d6daea Windows: statement_dispute_hours=120h, evidence_hours=48h, buyer_confirmation_hours=120h Acceptance criteria: - c1 (deterministic, required): Calendar event confirmed with at least one external attendee Check: {"and":[{"==":[{"var":"connector.gcal.status"},"confirmed"]},{">=":[{"var":"connector.gcal.attendee_count_external"},1]}]} - c2 (deterministic, required): Attendance confirmed Check: {"or":[{"==":[{"var":"connector.attendance.confirmed"},true]},{"==":[{"var":"connector.buyer.confirmed_attendance"},true]}]} - c3 (deterministic, required): Meeting lasted at least the minimum duration Check: {">=":[{"var":"fields.duration_minutes"},{"var":"terms.min_duration"}]} - c4 (deterministic, required): Not a duplicate by event id or by prospect and week Check: {"and":[{"==":[{"var":"connector.recourse.meeting_id_matches"},0]},{"==":[{"var":"connector.recourse.prospect_week_matches"},0]}]} - c5 (deterministic, material): Scheduled within the period and booked no more than 45 days before the start Check: {"and":[{"within_window":[{"var":"fields.scheduled_start"},{"var":"time.period_start"},{"var":"time.period_end"}]},{"<=":[{"var":"time.booked_lead_days"},45]}]} - r1 (rubric, material): Prospect matches the target profile Question: Does the prospect match the target profile in the terms by title and company? Passes when: The prospect's role and company fall within the target profile described in the terms. Evidence: prospect_email, prospect_domain Evidence artifacts: recording_or_notes (optional, audio/mpeg | video/mp4 | text/plain | text/markdown | application/json, max 104857600 bytes) Evidence fields, required: meeting_id, prospect_email, prospect_domain, booked_at, scheduled_start, scheduled_end, attended, duration_minutes Evidence fields, optional: none Exclusions: Meetings with existing customers or open opportunities in the buyer's CRM at booking time (buyer supplies hashed list). Reschedules count once. Gaming defenses: Vendor-affiliated domains detected. No-show handling: a no-show is not a unit. Reschedule chains collapse to one unit. A required criterion that fails rejects the unit. Read the full standard at https://recourse-mu.vercel.app/standards#OS-3. ### CD-0 Custom Deliverable (v0.4.0) A schema for one-shot deliverables. The vendor defines acceptance criteria using the acceptance criterion schema in the technical specification §5. Recourse requires at least one required deterministic criterion and rejects criteria that are not testable (R-520 applies against the drafter). Unit: One deliverable as described in the terms. Dedupe key: deliverable_id Standard hash: sha256:e4e5a667c05365c2c6f2aeab392e2a9f6d8e13c9a958630dd941897a05df0e87 Windows: statement_dispute_hours=120h, evidence_hours=48h, buyer_review_hours=120h Acceptance criteria: - c1 (deterministic, required): Submitted before the deadline plus grace Check: {"==":[{"var":"time.submitted_before_deadline"},true]} Evidence artifacts: deliverable (required, application/octet-stream | application/zip | application/pdf | text/plain | text/csv | application/json, max 1073741824 bytes) Evidence fields, required: deliverable_id, delivered_at Evidence fields, optional: none Exclusions: none Gaming defenses: At least one required deterministic criterion is enforced at terms validation. Criteria that are not testable as written are rejected before acceptance; ambiguity in accepted terms is read against the drafter (R-520). A required criterion that fails rejects the unit. Read the full standard at https://recourse-mu.vercel.app/standards#CD-0. ## 8. Reason codes Every ruling cites a rule id and a reason code from the rulebook version in force when the terms were accepted. Rulebook 0.4.0 defines 24 codes. - R-000 No dispute: Released by agreement or by lapse of the review window. - R-101 Delivery not received: No submission by the deadline plus grace; buyer prevails; hold refunded. - R-102 Delivery after deadline: Evidence timestamps fall outside the delivery window; buyer prevails on the late units unless terms allow late delivery at a reduced price. - R-103 Outcome outside period: An outcome claimed for a period other than the one in which it occurred; unit moved to the correct period or rejected if outside the term. - R-201 Required criterion failed: A required acceptance criterion is not met; unit rejected. - R-202 Material criteria failed: Two or more material criteria failed; unit rejected. - R-203 Minor criteria failed: Only minor criteria failed; unit verified; noted on the record. - R-210 Acceptance criterion failed on source of truth: The connector's record shows the criterion unmet. - R-305 Evidence integrity: Artifact hash does not match the signed manifest; unit rejected. - R-306 Source of truth conflict: Party-submitted evidence conflicts with the connector's record; the connector prevails. - R-311 Duplicate outcome: Dedupe key matches a previously verified unit; the duplicate is rejected. - R-312 Self-dealing indicators: The "external" party shares identifiers with the vendor; unit referred to review; if confirmed, rejected and recorded. - R-320 Manipulative evidence: Evidence contained instruction-like content that altered a verdict on re-run; unit rejected and recorded. - R-330 Unrelated evidence: Evidence does not relate to the unit; disregarded. - R-401 Non-response, buyer: Buyer neither accepted nor disputed within the review window; funds released to the vendor. - R-402 Non-response, vendor: Vendor added no evidence in the evidence window; the buyer's claim is upheld on the disputed units. - R-410 Withdrawn dispute: Disputing party withdrew; its bond is released less the dispute fee. - R-450 Bond escalation, disputes: A party that has lost three disputes in 90 days posts a doubled bond thereafter. - R-451 Bond escalation, rejections: A vendor whose rejected units exceed 20% in a period has sampling doubled and statement release delayed to the window end. - R-501 Terms mismatch: The claimed unit is not within the terms' standard or scope; rejected. - R-520 Terms ambiguity: A criterion is not testable as written; it is read against the party that drafted the terms. - R-530 Partial performance: Deliverable partially meets terms; split ruling with a stated apportionment. - R-600 Panel decision: Ruling issued by the bonded panel on appeal; cites the rules applied. - R-900 Rail event: A chargeback, payout failure, or account restriction on the rail suspended releases; not a determination on the merits. 6 of these are applied deterministically, before any model runs. Read the procedure at https://recourse-mu.vercel.app/rulebook. ## 9. Errors Every failure is RFC 9457 problem+json: `{ type, title, status, code, detail, request_id, next }`. Branch on `code`; it is stable. `detail` says what failed, `next` names the call that resolves it, and `request_id` is the one string to quote to support. - invalid_request (400): The body or the query did not match the schema. Next: Fix the fields named in detail and send the request again. The schemas are in /openapi.json. - unauthorized (401): No credential, or one that does not resolve. Next: Send Authorization: Bearer . An organization mints an agent key with POST /v1/parties/{id}/keys. - insufficient_scope (403): The key is valid and does not hold the scope this route needs. Next: Mint a key that holds the scope named in detail with POST /v1/parties/{id}/keys, then repeat the request. - wrong_party (403): The credential is a party to this commitment, but the wrong side of it. Next: Call as the party named in detail. GET /v1/whoami reports the party this credential acts as. - not_found (404): No such commitment, or none this credential may read. Next: Check the id. GET /v1/commitments lists the commitments this party is a party to. - unknown_party (404): The party named in the request does not exist. Next: Create the party with POST /v1/parties, then use the id it returns. - invalid_transition (409): The command is not legal from the state the commitment is in. Next: GET /v1/commitments/{id} for the current state, then send the command that state allows. - concurrent_modification (409): The commitment moved while this command was being applied. Next: GET /v1/commitments/{id} again and resend the command against the state you read. - terms_hash_mismatch (409): The hash being signed is not the current terms hash. Next: GET /v1/commitments/{id}, take current_terms_hash, and sign that hash. - terms_missing (409): The commitment has no valid terms on file. Next: Create the commitment again with its terms. POST /v1/commitments records them. - signature_invalid (400): The signature does not verify against the party key over the terms hash. Next: Sign the utf8 bytes of the lowercase hex terms hash with the party key and send the signature base64. - idempotency_key_required (400): A call that creates or moves money arrived without an Idempotency-Key. Next: Send an Idempotency-Key header with a value unique to this request, then repeat it verbatim to retry. - idempotency_key_reused (409): The same key arrived with a different body. Next: Send a new Idempotency-Key, or resend the identical body under the same key. - rail_unsupported (400): The rail named in the request is not the rail this commitment is on. Next: Fund on the rail the commitment names. GET /.well-known/recourse.json lists the rails this deployment carries. - unknown_standard (400): The terms cite an Outcome Standard this deployment does not serve. Next: GET /v1/standards for the standards this deployment serves, with their ids, versions, and hashes. - unknown_rulebook_version (400): The terms cite a rulebook version that is not published. Next: GET /.well-known/recourse.json for the rulebook version in effect and its hash. - unknown_reason_code (400): The reason code asked for is not in that rulebook version. Next: GET /v1/reason-codes for every reason code in the rulebook version that produced the ruling. - rate_limited (429): Too many requests in the window. Next: Wait for the window to pass and repeat the request unchanged. - method_not_allowed (405): The path exists and does not answer this method. Next: Use the method /openapi.json declares for this path. - unsupported_media_type (415): The body was not JSON. Next: Send application/json. - not_ready (503): The service is up and its database is not answering. Next: Retry with backoff. GET /ready reports when the service answers again. - internal_error (500): The request failed for a reason the API did not expect. Next: Retry with backoff. If it persists, quote request_id — the failure is recorded against it. - connection_failed (no reply): The SDK never reached the API: DNS, TCP, TLS, or offline. Nothing was recorded. Next: Retry with backoff. Send the same Idempotency-Key on a money call. - timeout (no reply): The SDK aborted the request before an answer arrived. The call may already have landed. Next: Retry with the same Idempotency-Key, then GET /v1/commitments/{id} to confirm the state. - invalid_response (no reply): The API answered with something other than the document the method asked for. Next: Do not retry blindly. GET /v1/commitments/{id} and branch on the state you read. ## 10. Verifying it yourself Nothing here requires trusting Recourse. Every claim on a record is checkable from published material with a SHA-256 and an Ed25519 verify. 1. Recompute the terms hash. terms_hash = sha256(JCS(terms)), lowercase hex, rendered on the wire as sha256:. Canonicalize the terms document with RFC 8785 and hash it. It matches, or the terms you are holding are not the terms that were signed. 2. Recompute the standard hash. Every Outcome Standard is a published JSON file. sha256(JCS(file)) is the hash a commitment cites. A standard that changed would change its hash, and the commitment cites the version it was accepted under. 3. Walk the event chain. hash = sha256(JCS({stream_id, seq, type, payload, occurred_at, prev_hash})), where prev_hash is the bare hex of the event before it and null at seq 1. Strip the sha256: prefix the API renders before you hash. A missing or altered event breaks the chain at that seq. 4. Check the log signature. Each event carries an Ed25519 signature, base64, over the utf8 bytes of its bare lowercase hex hash. The raw public key is on the response as log_public_key and in the published keys document. Verify against the published key, not the one on the response, if you want the stronger check. ```ts import { createHash, createPublicKey, verify } from "node:crypto"; import { canonicalize } from "@recourse/crypto"; // or any RFC 8785 implementation const sha256 = (value: unknown): string => createHash("sha256").update(canonicalize(value), "utf8").digest("hex"); // 1 — The terms hash the buyer signed. sha256(terms) === commitment.terms_hash?.replace(/^sha256:/, ""); // 2 — The chain. On the wire a hash reads "sha256:"; the chain is hashed // over the bare hex, and prev_hash is null at seq 1. const stream = await client.commitments.events(commitment.id); const publicKey = createPublicKey({ key: Buffer.concat([ Buffer.from("302a300506032b6570032100", "hex"), // Ed25519 SPKI prefix Buffer.from(stream.log_public_key, "base64"), ]), format: "der", type: "spki", }); let prev: string | null = null; for (const event of stream.data) { const hash = sha256({ stream_id: event.stream_id, seq: event.seq, type: event.type, payload: event.payload, occurred_at: event.occurred_at, prev_hash: prev, }); if (hash !== event.hash.replace(/^sha256:/, "")) { throw new Error(`the chain breaks at seq ${event.seq}`); } // 3 — The log signature: Ed25519 over the utf8 bytes of the hex hash. const signed = verify( null, Buffer.from(hash, "utf8"), publicKey, Buffer.from(event.signature, "base64"), ); if (!signed) throw new Error(`the signature fails at seq ${event.seq}`); prev = hash; } ``` ## 11. Fees and rails The platform fee is 1% of released value, taken on release and nowhere else; the first $10,000 released per vendor is free. A dispute costs $0.10 per disputed outcome, or $10 for a statement dispute, retained from the losing side's bond. The buyer pays Recourse nothing. Rails are adapters and Recourse never custodies funds. The manual rail ships first: it records the instruction and its state, and money moves between the parties on their own arrangements. Stripe, Circle, and x402 adapters follow. A commitment reports the rail it is on; the hosted contract page says so plainly. ## 12. MCP An agent whose client speaks MCP can use `@recourse/mcp` instead of HTTP. It runs over stdio and holds the API key in its own environment, so the key never enters a prompt or a tool argument. The package is not published to a registry yet; run it from a clone of the repository. ```json { "mcpServers": { "recourse": { "command": "pnpm", "args": ["--filter", "@recourse/mcp", "dev"], "env": { "RECOURSE_API_URL": "https://api.recourse.dev", "RECOURSE_API_KEY": "rk_live_…" } } } } ``` ## 13. Scope No custody: Recourse instructs holds, releases, and refunds on the parties' own rails. No guarantee: Recourse holds, verifies, rules, and records, and does not underwrite the deal. Rulings are machine-first and always cite a rule; a losing party may appeal to a panel. Records are never edited and never deleted. Read the Rulebook at https://recourse-mu.vercel.app/rulebook, the standards at https://recourse-mu.vercel.app/standards, and the quickstart at https://recourse-mu.vercel.app/docs. The OpenAPI document is at https://api.recourse.dev/openapi.json.