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.

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)

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 > now

Matching 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.