Pipe
The one-sentence version
Pipe is how a smart contract on the blockchain asks Plumb an AI question and gets an answer back that it can trust.
Why this is hard
Blockchains don't have the internet. A contract running on Base can't open a browser tab, call an API, or hit a model server — every bit of data it sees has to come from the chain itself.
So if a developer wants their smart contract to use AI — "classify this image," "score this text," "summarize this document" — they need a way to bring an answer from the outside world onto the chain. That piece of middleware is called an oracle. Pipe is Plumb's oracle.
The flow
Think of it like ordering food at a restaurant when you're not allowed to leave your table:
- The contract places an order. It says "I want the answer to this question" and hands over an ID.
- Plumb's worker notices the order. It grabs the question from off-chain storage, runs the AI model, and produces an answer.
- Plumb signs the answer and delivers it back. The contract is handed the answer along with a cryptographic signature proving Plumb really produced it.
From the contract's point of view, it placed an order and later received a trustworthy result. From Plumb's side, it's just another AI request — plus a signed receipt and an on-chain delivery.
What actually goes on the chain
Putting a full AI model or a full input onto a blockchain would be wildly expensive — a small model is megabytes, a large one is gigabytes, and blockchain storage is measured in pennies per byte. So Plumb only puts fingerprints on the chain:
- Model fingerprint — a short ID that points to a specific model stored in the Plumb Hub.
- Input fingerprint — a short ID pointing to the input data (the image, the text, the prompt) uploaded to Plumb's servers.
The contract commits those two fingerprints. The worker looks them up, fetches the real bytes off-chain, runs the model, and returns the result. Everyone stays honest because any tampering would change the fingerprint and break the signature.
Before you can call Pipe
Two setup steps, done once per model and once per input:
- Register the model in Plumb Hub. Upload your model file; Hub gives you back its fingerprint. See Hub.
- Upload your input data. Send your image / text / bytes to Plumb; get back the input fingerprint.
Then your contract can place an order using those two fingerprints:
oracle.request(modelFingerprint, inputFingerprint, callbackAddress);
Without both pieces prepared, the worker will mark the job failed with a reason like model_unknown or input_missing.
Safety: what could go wrong and how Pipe handles it
Oracles are notoriously easy to mess up. A few things Pipe specifically defends against:
Replay attacks
The risk: Plumb signs an answer. The contract trying to receive it fails for some reason. The signed answer is now floating around and might get replayed to the contract later.
Pipe's fix: The oracle contract marks the job "done" before calling the contract back. Even if the callback fails, the job stays marked done forever — the same signed answer can never be submitted again.
A badly-written contract wastes resources
The risk: A buggy consumer contract has a bad bug in its callback — maybe an infinite loop. Without guardrails, that could drain the oracle's funds trying to deliver the answer.
Pipe's fix: The callback gets a hard limit on how much computing it can do. If it misbehaves, the oracle stops forwarding gas, records that the callback failed (so the developer can see what happened), and moves on.
Someone impersonates the oracle
The risk: Another address pretends to be the Plumb oracle and sends a fake result to your contract.
Pipe's fix (you do this): Your contract has to check that the person calling it is actually the real Plumb oracle. One line of Solidity:
if (msg.sender != address(oracle)) revert NotOracle();
Without that check, anyone could forge results. With it, only Plumb's real oracle can write to your contract.
Example: a contract that uses Pipe
// 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; }
// 1. Ask a question
function ask(bytes32 modelHash, bytes32 inputHash) external returns (uint256) {
return oracle.request(modelHash, inputHash, address(this));
}
// 2. Receive the answer
function pipeCallback(uint256 jobId, bytes calldata result) external {
if (msg.sender != address(oracle)) revert NotOracle();
results[jobId] = result;
}
}
That's a fully-working example. Pick a model from the Hub, upload some input data, call ask, and your contract will receive an AI-generated answer back at pipeCallback.
What's free and what costs money
- Running AI models through Pipe is free right now. Pipe is a preview feature — there's no per-job charge today. Once usage patterns settle, pricing will be added in a future release.
- Regular blockchain gas still applies. Your contract pays for the request transaction as normal, and receives the callback as a standard on-chain call.
- Inputs and models stored on Plumb follow the usual Hub / gateway pricing.
The honest limits
- Pipe currently runs on Base Sepolia, which is a testnet. It's free to use and good for development, but it's not production money. Migration to Base mainnet is on the roadmap.
- Pipe can't prove the model was actually run correctly. Same caveat as everything else in Plumb — the signature proves what the operator committed to, not that the operator is fully honest. The TEE attestation feature on the roadmap closes this gap.
- Models are what's in the Hub. If the model you want isn't registered, you have to upload it first. There's no Pipe-native list of "available models" that exists independently of Hub.
For developers
The oracle contract is PipeOracle.sol. request(bytes32 modelHash, bytes32 inputHash, address callback) returns (uint256 jobId) emits JobRequested(id, requester, modelHash, inputHash).
The worker's pipe-fulfiller loop in apps/worker/src/pipe-fulfiller.ts consumes JobRequested events via the watcher, fetches input bytes from /pipe/inputs/<inputHash> (filesystem-backed), fetches model bytes from the Hub via modelHash, runs onnxruntime with a 30s timeout, computes resultHash = keccak256(result), EIP-191-signs keccak256(abi.encode(oracleAddress, chainId, "FULFILL", id, resultHash)) with the shared settlement signer, and submits via runExclusive(signer.address, …) to the per-signer tx queue (same queue that serializes checkpoint submissions — one signer, one nonce stream).
PipeOracle.fulfill(id, result, sig) recovers the signer, sets fulfilled = true before calling the consumer, emits JobFulfilled, then does callback.call{gas: CALLBACK_GAS_STIPEND} where CALLBACK_GAS_STIPEND = 100_000. Callback revert is caught and emitted as CallbackFailed(id, data); fulfilled persists so replay is impossible (AlreadyFulfilled(id) reverts). The 100k stipend defends against gas griefing — test_fulfill_gasGriefingCallbackCannotPreventFulfill exercises this with a deliberately malicious consumer.
Preconditions for a request to succeed: the model must be registered in HubRegistry at modelHash, and input bytes must exist at /pipe/inputs/<inputHash>. The worker marks failed jobs with reason = model_unknown | input_missing | timeout | … and never issues a receipt for them.
Current pricing: cost_micro = 0 on all pipe receipts. Hub inference accounting happens upstream of pipe.