§ P.03 · Verification & Pipe

Trust, but verify.
Call inference from a contract.

Plumb gives you two independent verification paths: one for off-chain callers (the ed25519 receipt you verify in your code) and one for on-chain callers (the PipeOracle contract, where a Solidity consumer requests inference and the oracle delivers a signer-cosigned result).

V.01

The canonical payload

Every receipt is signed over a fixed, newline-separated text format — not JSON, not a struct. The payload order and field names never change; the v1 prefix is the versioning mechanism.

canonical payload · v1
v1
request_hash=0x7a1b2c...
response_hash=0xabcdef...
model=anthropic/claude-sonnet-4.5
cost_micro=482000
issued_at=2026-04-22T18:30:15.120Z

Any divergence — different case on the model id, trailing whitespace, millisecond precision change — produces a different signature. The spec is pinned across the TS server, browser clients, and Python SDK with a shared fixture of known-vector inputs + expected sha256 hashes that every implementation has to match.

V.02

Client-side verify (3 lines of Python)

example · python · verify_receipt
from plumb_sdk import Client

c = Client(base_url="https://api.plumbtech.xyz")
rcpt = c.receipts.get("rc_01HVBZ...")

assert c.verify_receipt(rcpt), "signature did not match canonical payload"
print("verified", rcpt.id, "·", rcpt.cost_micro, "μPLMB")

verify_receipt rebuilds the canonical payload from your copy of the receipt, fetches the public key for signer_key_id from the key registry, and runs ed25519. The explorer does the same in-browser via @noble/ed25519 — click the "Verify" button on any receipt page.

V.03

Signer rotation

The active signer key can be rotated via PLUMB_SIGNER_KEY_ID + PLUMB_SIGNER_KEY_ED25519. Past receipts stay verifiable forever — the key registry loads a list of historical keys from PLUMB_SIGNER_HISTORICAL_KEYS_JSON so a receipt with signer_key_id = srv-2026-03 can still be verified after rotation to srv-2026-05. The /v1/verification/keys endpoint publishes all of them with their validity windows.

V.04

PipeOracle: inference for contracts

A Solidity contract calls oracle.request(modelHash, inputHash, callback). The oracle emits JobRequested, the Plumb worker picks up the event, fetches the input bytes by hash, runs inference, and submits fulfill(jobId, result, sig) — an EIP-191 signature over keccak256(address, chainId, "FULFILL", jobId, resultHash).

The oracle recovers the signer, stores fulfilled = true before calling the consumer, and invokes pipeCallback(jobId, result) via a low-level call capped at 100_000 gas. A reverting consumer emits a CallbackFailed(id, reason) event but doesn't undo the fulfilled state — so the same signed result can't be replayed later. Gas griefing is bounded by the stipend.

V.05

Minimal on-chain consumer

example · solidity · IPipeCallback implementer
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {IPipeOracle, IPipeCallback} from "@plumb/contracts/src/IPipeOracle.sol";

contract Example is IPipeCallback {
    IPipeOracle public oracle;
    mapping(uint256 => bytes) public results;

    error NotOracle();

    constructor(IPipeOracle _oracle) { oracle = _oracle; }

    function ask(bytes32 modelHash, bytes32 inputHash) external returns (uint256) {
        return oracle.request(modelHash, inputHash, address(this));
    }

    function pipeCallback(uint256 jobId, bytes calldata result) external {
        if (msg.sender != address(oracle)) revert NotOracle();
        results[jobId] = result;
    }
}

↳ The input bytes are uploaded out-of-band via POST /pipe/inputs before the contract call — the hash committed on-chain is the keccak256 of those bytes. See Pipe details.

V.06

Roadmap: TEE attestation + zkML

Receipts today carry verification_mode: "vanilla" — the server runs inference, the server signs. Attested modes are specified but gated behind hardware: tee-nitro and tee-sgx will ride a TEE attestation quote alongside the signature, and verifiers check both. zkml is further out and scoped to small-enough models. The interface is ready (Attester is already abstract behind StubAttester), the hardware integration is the remaining work.

GUIDE

Verification deep dive
Pipe oracle details
Deployed contract addresses