Technical deep dive
How Stvor works
The $1.5B Bybit hack was a tampered payload. Same root cause: execution happened before anyone verified the live operation still matched intent. Stvor binds intent to execution at the checkpoint — before the payment rail fires. A swapped destination is a signed DENY, not a silent pass.
Overview
Stvor is middleware between intent and execution. It does not replace custody, monitoring, or signing infrastructure. It answers: does what is about to run still match what was committed? Any check fails → DENY with a signed receipt inline from POST /verify, no payment attempted.
01 · commit → verify → settle
POST /commitments
POST /verify
timingSafeEqual. Destination swap or hash mismatch → DENY.Your execution rail
POST /verify. Optionally call POST /receipt with txHash after settlement.02 · What verify checks
One gate: SHA-256(JCS(live payment payload)) compared to the stored payloadHash with crypto.timingSafeEqual(). Payment payload fields are to, amount, currency, and optional chain/ asset. Swap the destination or amount after commit → signed DENY.
03 · Payload attestation
Commit–reveal pattern: hash the canonical task payload at intent time. Before execution, re-hash the live payload and compare. A single byte change produces a different digest.
Attack
Bybit — $1.5B — Feb 2025
- 1.Safe{Wallet} developer workstation compromised
- 2.Malicious JS injected: UI shows correct destination, tx silently swapped
- 3.Signers approve what they see — sign something else
✗ $1.5B transferred in one routine-looking operation
Stvor
Blocked at verify
- 1.SHA-256(task_payload) committed at intent — immutable before UI renders
- 2.timingSafeEqual(received_hash, committed_hash) before execution
- 3.Mismatch → signed DENY. Stripe PaymentIntent.cancel() if escrowed.
✓ Attacker gets nothing. Receipt proves the block.
import crypto from "node:crypto";
function canonicalize(params: Record<string, unknown>) {
return JSON.stringify(params, Object.keys(params).sort());
}
function verifyPayload(
liveParams: Record<string, unknown>,
commitment: { payloadHash: string },
) {
const hash = crypto.createHash("sha256")
.update(canonicalize(liveParams))
.digest("hex");
const a = Buffer.from(hash, "hex");
const b = Buffer.from(commitment.payloadHash, "hex");
if (a.length !== b.length) return { decision: "DENY", reason: "PAYLOAD_MISMATCH" };
if (!crypto.timingSafeEqual(a, b)) return { decision: "DENY", reason: "PAYLOAD_MISMATCH" };
return { decision: "ALLOW" };
}04 · Intent-to-execution binding
Slow-accumulation attacks (fake tokens, approval farming) are a different class of problem than payload swaps. Stvor's live gate answers one question: did this specific execution still match the committed payment payload?
05 · Stripe reference flow
The live reference implementation uses Stripe capture_method: manual. Funds authorize at funding but do not capture until verify returns ALLOW. This is one rail — not the product.
06 · Trust Receipt
After ALLOW or DENY, POST /verifyreturns an ES256 (P-256, IEEE-P1363) signed Trust Receipt inline (ATS-1 draft). Verifiable offline with only the issuer's public key — browser verifier, CLI, or ~20 lines of WebCrypto.
{
"receiptId": "rec_41gdRVEv7_Yo",
"binding": "agent-committed",
"agentId": "agent_demo_blocked",
"decision": "DENY",
"reason": "PAYLOAD_MISMATCH",
"to": "0x8f3a2c91d4e7b0a6c5d8f1e2a4b7c9d0e3f6a8c1d",
"amount": "50000.00",
"currency": "USDC",
"kid": "key_xd4o8VukjEgAyNPg",
"signature": "rQ3nsY5ghE3QT7acZGfWTmZJkoKkNgnE30CH-d2Ie22dAmBGW_mf49e7o1NOsn0EayW63fzbe_ZJhvdsfzXiuA"
}07 · Threat model
Payload manipulation (in scope)
Attacker modifies destination, amount, or currency between commit and execution. Stvor catches this because the modified payload produces a different SHA-256 digest → signed DENY before settlement.
Out of scope
Scope revocation (authorization expired in March, execution in April), slow approval farming, prompt injection that poisons the commit itself, key compromise, and humans who bypass the checkpoint entirely.