live · mainnetoc · docs
specs · api · guides
docs / bonded reputation

Bonded reputation on fleet

Fleet's bonded-reputation surface is a managed wrapper around the open oc-pledge-protocol. The load-bearing properties (canonical id, BIP-322 signature, public Nostr publication, offline verifiability) are all in the protocol; fleet adds persistence, dashboard surfaces, audit-bundle inclusion, and webhooks for operators who want one place to see the loop.

Trust model unchanged. Every pledge fleet persists is also published to the public Nostr relay set. Anyone can verify any pledge offline against the swearer's BIP-322 key without trusting fleet — fleet is one source of truth among many, not the source.

The loop

   compose ── sign ── publish ── persist ── surface ── verify
   /reputation   wallet   nostr     fleet      dashboard   anyone
   /compose                         /api/pledges          /verify
  1. Compose. /reputation/compose walks the operator through proposition, resolution mechanism, bond, and timing. The composer derives the canonical message + id locally; nothing is sent to fleet until step 4.
  2. Sign. The wallet (UniSat / Xverse / Leather / OKX / Phantom or pasted) produces a BIP-322 signature over the canonical message. Fleet never sees the private key.
  3. Publish. The signed envelope is posted to the family's Nostr relay set as a kind 30078 event with oc-pledge:<id> as the d tag.
  4. Persist. The envelope is POSTed to /api/pledges so it shows up in fleet surfaces. The server re-derives the id from canonical inputs and rejects mismatches (tamper guard); BIP-322 is not re-verified server-side because the published Nostr envelope is verifiable offline. Fires a pledge.registered webhook.
  5. Surface. Three views:
    • Dashboard. /dashboard → § bonded reputation card lists the 5 most-recent pledges with status pills.
    • Project listing. /reputation/list is the full filterable table (status filter + free-text search). Each row clicks through to the canonical detail page.
    • Audit timeline. /audit merges pledges into the same timeline as delegations / actions / revocations / admin events. The pledge filter isolates the slice.
  6. Verify. Three paths:
    • Public detail. /reputation/p/<id> shows the envelope, the canonical message, the verify-offline recipe, and a "open at pledge.ochk.io" cross-link.
    • In-browser verifier. /verify accepts all six OC envelope kinds (delegation / action / revocation / pledge / pledge-outcome / pledge-abandonment); paste the JSON, get a verdict.
    • CLI / SDK. Any @orangecheck/pledge-core@^1.0.0 consumer; see the SDK reference.

API endpoints

The full schemas live in the OpenAPI spec; summary:

Pledges (V1)

MethodPathAuthWhat
POST/api/pledgessessionRegister a signed pledge envelope. Recomputes id, rejects tampered.
GET/api/pledges?project_id=…sessionList pledges for the project. Optional status filter.
GET/api/pledges?swearer_address=…publicCross-fleet list of every pledge sworn by an address. No auth.
GET/api/pledges/{id}publicFetch one pledge by envelope id. No auth — pledges are public artifacts.

Outcomes (V2)

Outcome envelopes resolve a pledge to kept / broken / expired_unresolved / disputed. Deterministic mechanisms (chain_state, nostr_event_exists, stamp_published, http_get_hash, dns_record, vote_resolves) emit unsigned outcomes (envelope.sig=null); the counterparty_signs mechanism requires a real BIP-322 from the counterparty's address. The session caller can also publish an outcome under their own address (e.g. swearer signing broken to acknowledge a missed commitment). Disagreement is allowed — multiple outcomes stack on the parent pledge and the state machine surfaces them as disputed until the dispute window ends.

MethodPathAuthWhat
POST/api/pledges/{id}/outcomesessionRegister a signed outcome envelope. Recomputes id, enforces sig rules + resolved_by authorization.
GET/api/pledges/{id}/outcomepublicEvery outcome envelope targeting this pledge_id. Multiple rows possible.
GET/api/pledge-outcomes?project_id=…sessionCross-pledge outcome list for a project. Used by the audit timeline.
GET/api/pledge-outcomes?swearer_address=…publicEvery outcome where the parent pledge's swearer matches.

Abandonments (V2)

Per SPEC §5.2, only the original swearer can abandon a pledge (no agent abandonments in v0.1) and the action is permanent. A second abandonment on the same pledge is a silent no-op.

MethodPathAuthWhat
POST/api/pledges/{id}/abandonsessionRegister a swearer-signed abandonment. Server enforces sig.pubkey === swearer.
GET/api/pledges/{id}/abandonpublicFetch abandonment if present (404 otherwise).
GET/api/pledge-abandonments?project_id=…sessionProject-scoped abandonments list.
GET/api/pledge-abandonments?swearer_address=…publicPublic per-swearer abandonments list.

Webhook events

Subscribe via POST /api/webhooks/endpoints with the events you care about. Three pledge-family events fire today:

EventWhen
pledge.registeredA pledge envelope was accepted by /api/pledges. Payload: id, project_id, kind, envelope, swearer_address, status.
pledge.outcomeAn outcome envelope was accepted by /api/pledges/{id}/outcome. Payload: id, project_id, kind, pledge_id, outcome, resolved_by, envelope.
pledge.abandonedAn abandonment envelope was accepted by /api/pledges/{id}/abandon. Payload: id, project_id, kind, pledge_id, envelope.

Sample pledge.outcome payload:

{
    "id": "<64-hex outcome envelope id>",
    "project_id": "proj_…",
    "kind": "pledge-outcome",
    "pledge_id": "<64-hex parent pledge id>",
    "outcome": "kept",
    "resolved_by": "deterministic",
    "envelope": {
        /* full canonical OutcomeEnvelope */
    }
}

Audit bundle inclusion

The signed audit-bundle export at /api/audit/export includes the full V2 lifecycle in all three formats (JSON, NDJSON, CSV) alongside delegations / actions / revocations. A compliance team rebuilds the project's agent-state-machine from the bundle, then projects pledges on top, then resolves their lifecycle by applying outcomes + abandonments — all from the same content-addressed envelopes the operator signed, byte-identical to what's on the public Nostr relay set.

Bundle replay order: delegations → revocations → actions → pledges → outcomes → abandonments. Manifest counts: delegations, revocations, actions, pledges, outcomes, abandonments. The audit_bundles bookkeeping table tracks all six counts so the dashboard's bundle list summarizes without re-loading the tarball.

Verifying offline

Every pledge detail page at /reputation/p/<id> includes a copy-paste Node recipe; the canonical version:

// npm i @orangecheck/pledge-core
import { verifyPledge } from '@orangecheck/pledge-core';

const res = await fetch(`https://fleet.ochk.io/api/pledges/${id}`);
const { pledge } = await res.json();

const result = await verifyPledge({
    envelope: pledge.envelope,
    // wire your BIP-322 verifier here — bip322-js, btc-signer, etc.
    skipSignatureVerification: true,
});

console.log(result.ok ? 'ok' : `FAIL: ${result.code}`);

ok=true proves: (1) declared id matches the canonical hash of inputs, (2) the BIP-322 signature is valid against the swearer's address (when the verifier callback is wired), (3) envelope is structurally well-formed. It does not prove the pledge has been kept — that's an outcome envelope, separately signed.

Activation

Pledge persistence is gated behind a Postgres migration (drizzle/0004_*). On a fresh deploy or a deploy that hasn't pulled the latest migration, every pledge endpoint returns 503 schema_not_migrated and the dashboard falls through to a "preview" banner. Run yarn db:migrate against the deploy's DATABASE_URL to flip it on. See STATUS.md for the runbook.

What fleet does not do

  • Slashing. Fleet does not move sats. Bonded reputation is enforcement by exposure: a broken pledge attaches a permanent record to the swearer's Bitcoin address. The bond stays in the wallet.
  • Custody. Fleet does not hold any private keys. Every pledge is signed on the operator's wallet; fleet receives an already-signed envelope.
  • Aggregated scores. Fleet does not compute a "reputation score." The primitive is raw replayable history — anyone can compute their own aggregation from the canonical pledge stream.