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
- Compose.
/reputation/composewalks 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. - 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.
- Publish. The signed envelope is posted to the family's Nostr relay set as
a kind 30078 event with
oc-pledge:<id>as thedtag. - Persist. The envelope is POSTed to
/api/pledgesso 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 apledge.registeredwebhook. - Surface. Three views:
- Dashboard.
/dashboard→ § bonded reputation card lists the 5 most-recent pledges with status pills. - Project listing.
/reputation/listis the full filterable table (status filter + free-text search). Each row clicks through to the canonical detail page. - Audit timeline.
/auditmerges pledges into the same timeline as delegations / actions / revocations / admin events. Thepledgefilter isolates the slice.
- Dashboard.
- 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.
/verifyaccepts 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.0consumer; see the SDK reference.
- Public detail.
API endpoints
The full schemas live in the OpenAPI spec; summary:
Pledges (V1)
| Method | Path | Auth | What |
|---|---|---|---|
POST | /api/pledges | session | Register a signed pledge envelope. Recomputes id, rejects tampered. |
GET | /api/pledges?project_id=… | session | List pledges for the project. Optional status filter. |
GET | /api/pledges?swearer_address=… | public | Cross-fleet list of every pledge sworn by an address. No auth. |
GET | /api/pledges/{id} | public | Fetch 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.
| Method | Path | Auth | What |
|---|---|---|---|
POST | /api/pledges/{id}/outcome | session | Register a signed outcome envelope. Recomputes id, enforces sig rules + resolved_by authorization. |
GET | /api/pledges/{id}/outcome | public | Every outcome envelope targeting this pledge_id. Multiple rows possible. |
GET | /api/pledge-outcomes?project_id=… | session | Cross-pledge outcome list for a project. Used by the audit timeline. |
GET | /api/pledge-outcomes?swearer_address=… | public | Every 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.
| Method | Path | Auth | What |
|---|---|---|---|
POST | /api/pledges/{id}/abandon | session | Register a swearer-signed abandonment. Server enforces sig.pubkey === swearer. |
GET | /api/pledges/{id}/abandon | public | Fetch abandonment if present (404 otherwise). |
GET | /api/pledge-abandonments?project_id=… | session | Project-scoped abandonments list. |
GET | /api/pledge-abandonments?swearer_address=… | public | Public per-swearer abandonments list. |
Webhook events
Subscribe via POST /api/webhooks/endpoints with the events you care about.
Three pledge-family events fire today:
| Event | When |
|---|---|
pledge.registered | A pledge envelope was accepted by /api/pledges. Payload: id, project_id, kind, envelope, swearer_address, status. |
pledge.outcome | An outcome envelope was accepted by /api/pledges/{id}/outcome. Payload: id, project_id, kind, pledge_id, outcome, resolved_by, envelope. |
pledge.abandoned | An 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.