Signed Price Proofs
Think of a notary's stamp. Before two agents agree a price, one can show the other what the official records said, with ProofPrice's stamp on it. The other agent does not need to trust the first one: it checks the stamp, for free, in a second.
What you get
Every price answer with status ok and every price check with a real verdict (from pricepaid_check_price or GET /api/v1/check) comes with a proof: a compact JWS signed with Ed25519. The answer itself does not change; the proof travels beside it.
- REST: the header
X-ProofPrice-Proof: <proof>, andLink: <https://proofprice.com/p/<proof>>; rel="proof", a page a person can open. - MCP: the tool result's
_meta["proofprice/proof"]holds{ jws, verify_url, view_url }.
A proof says: which question was asked, the figures (median, typical range, number of records, period, price type), the verdict and price for a check, the source and its licence, whether it came from our own copy because the official source did not respond, a SHA-256 hash of the full answer, when it was signed and when it expires (90 days later). And always this sentence:
ProofPrice signed what the official records it cites showed at this time. Evidence, not a valuation, guarantee or offer.
Errors, “no matches”, “not enough data” and “cannot judge” are never signed. We sign only what the records showed.
Check a proof (free, no key)
- API:
GET /api/v1/proofs/verify?proof=<proof>orPOST /api/v1/proofs/verifywith{"proof": "<proof>", "answer": <optional full answer>}. It answersvalid,reason,expired,kid, the signedclaimsand a plain-Englishhuman_summary. Send the answer too andanswer_matchessays whether it is exactly the one that was signed. Never charged, CORS open, nothing stored. - People: open
https://proofprice.com/p/<proof>. - Offline: our public keys are at /.well-known/proofprice-keys.json (JWKS,
kty OKP,crv Ed25519,alg EdDSA). Keys that no longer sign stay listed so older proofs keep checking.
curl
# 1. Ask, and keep the proof from the response header
curl -sD headers.txt -o answer.json \
'https://proofprice.com/api/v1/check?category=fuel&country=IE&fuel=diesel&price=1.89'
PROOF=$(grep -i '^x-proofprice-proof:' headers.txt | cut -d' ' -f2 | tr -d '\r')
# 2. Anyone can check it, free
curl -s "https://proofprice.com/api/v1/proofs/verify?proof=$PROOF"
# 3. ...and confirm the full answer they were shown is the one that was signed
curl -s -X POST https://proofprice.com/api/v1/proofs/verify -H 'content-type: application/json' \
-d "{\"proof\":\"$PROOF\",\"answer\":$(cat answer.json)}"JavaScript (WebCrypto, no library)
// Check a ProofPrice proof offline with WebCrypto (browsers, Node 20+, Deno, Bun). No library needed.
async function verifyProofPrice(jws) {
const [h, p, s] = jws.split(".");
const dec = (x) => Uint8Array.from(atob(x.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0));
const header = JSON.parse(new TextDecoder().decode(dec(h)));
const claims = JSON.parse(new TextDecoder().decode(dec(p)));
const { keys } = await (await fetch("https://proofprice.com/.well-known/proofprice-keys.json")).json();
const jwk = keys.find((k) => k.kid === header.kid);
if (!jwk || header.alg !== "EdDSA" || header.typ !== "proofprice-proof+jws") return { valid: false };
const key = await crypto.subtle.importKey("jwk", { kty: "OKP", crv: "Ed25519", x: jwk.x }, { name: "Ed25519" }, false, ["verify"]);
const ok = await crypto.subtle.verify({ name: "Ed25519" }, key, dec(s), new TextEncoder().encode(h + "." + p));
const inDate = claims.exp > Date.now() / 1000;
return { valid: ok && inDate && claims.iss === "https://proofprice.com", claims };
}// Or with the jose library (npm i jose)
import { createRemoteJWKSet, compactVerify } from "jose";
const keys = createRemoteJWKSet(new URL("https://proofprice.com/.well-known/proofprice-keys.json"));
const { payload, protectedHeader } = await compactVerify(jws, keys);
const claims = JSON.parse(new TextDecoder().decode(payload));
// then check protectedHeader.typ === "proofprice-proof+jws", claims.iss === "https://proofprice.com" and claims.exp > nowMatching the full answer
answer_sha256 is the SHA-256 (hex) of the answer you received (for a check, the whole check result) written as canonical JSON: object keys sorted, no spaces, as in RFC 8785. Keep the answer with the proof and anyone can confirm the two belong together.
What a proof is not
It is evidence of what official records showed when ProofPrice signed it. It is not a valuation, a guarantee or an offer, and it does not say whether a price is fair for one particular item.