Web Receipts
After a transaction reaches SETTLED, the provider's deliverable is published as a Web Receipt: an EIP-712-signed JSON object pinned to IPFS, with its content hash anchored on-chain via the delivery EAS attestation.
This is the off-chain half of the trust model. The on-chain attestation says "provider delivered something with hash X for transaction Y at timestamp Z." The Web Receipt is the something: readable, verifiable, retrievable forever.
Schema
{
"version": "1.0",
"txId": "0xTRANSACTION…",
"provider": "0xPROVIDER_SCW…",
"consumer": "0xCONSUMER_SCW…",
"service": "translate",
"input": { "text": "Hello", "target": "es" },
"output": { "translated": "Hola" },
"metadata": {
"model": "claude-sonnet-4-6",
"modelVersion": "2026-03-01",
"deliveredAt": "2026-05-26T12:00:00Z",
"computationMs": 230,
"customFields": { /* provider-defined */ }
},
"signature": "0xPROVIDER_SIGNATURE…",
"signedHash": "0xHASH_THAT_MATCHES_ON_CHAIN_ATTESTATION"
}
The signedHash must equal the attestationUid on-chain. If they diverge, the receipt is invalid.
SDK surface
In V1, receipt upload happens automatically as part of the DELIVERED transition on the provider side; no explicit call needed. The CLI tooling lives at src/cli/receiptUpload.ts for manual re-publish scenarios, and the uploadReceipt / fetchReceipt symbols are not exposed as top-level exports of @agirails/sdk@4.0.0. Fetching is done by IPFS CID directly:
- TypeScript
- Python
// V1 provider side: receipt upload is auto on DELIVERED. To re-publish
// manually, use the CLI: `npx actp receipt:upload --tx <txId> ...`
// or import the internal helper if your version exposes it.
// V1 consumer side: the on-chain pointer is tx.attestationUID (an EAS
// attestation UID), not a direct CID. Decode the EAS attestation to
// recover the receipt CID, then fetch from any IPFS gateway.
const txId = '0xTRANSACTION…';
const tx = await agent.client.standard.getTransaction(txId);
const attestationUID = tx?.attestationUID;
if (attestationUID) {
// const cid = await decodeAttestation(attestationUID); // your helper
// const url = `https://gateway.filebase.io/ipfs/${cid.replace('ipfs://', '')}`;
// const receipt = await fetch(url).then((r) => r.json());
// Verify signature against on-chain provider address yourself;
// signature recovery + signedHash comparison is not wrapped in V1.
}
# V1 Python: upload_receipt is exported; fetch_receipt is not yet in the
# public API. Fetch by CID via aiohttp / httpx against an IPFS gateway.
from agirails import upload_receipt
cid = await upload_receipt(tx_id=tx_id, output=result, metadata={...})
# Fetch:
import httpx
url = f"https://gateway.filebase.io/ipfs/{cid.replace('ipfs://', '')}"
async with httpx.AsyncClient() as http:
receipt = (await http.get(url)).json()
A canonical fetchReceipt / fetch_receipt helper that does the verification dance is on the V2 roadmap; the V1 path is "fetch by CID + verify the signature your way."
How it's pinned
The SDK calls agirails.app/api/v1/receipts (POST) which:
- Verifies the signature server-side against the on-chain provider address.
- Pins the JSON to IPFS via Filebase (Python SDK path) or Pinata (TS SDK path).
- Returns the IPFS CID + a shareable
https://receipts.agirails.app/r/{cid}URL. - Optionally also writes a pointer to the on-chain
WebReceiptRegistry(cheap, single SSTORE, can be skipped to save gas).
The IPFS pin is permanent: even if agirails.app went away, anyone running an IPFS node could fetch the receipt by CID. The on-chain pointer is the discovery index that makes the CID findable from just the txId.
Privacy: what gets published
By default, the entire receipt (including input and output payloads) is public on IPFS. For workflows handling PII or sensitive prompts, encrypt the payload to the consumer's public key:
const cid = await uploadReceipt({
txId,
output: handlerResult,
encryption: {
method: 'eciesAesGcm',
recipientPubkey: consumerPubkey, // consumer's EOA public key
},
});
The on-chain attestation still proves delivery happened (it commits to the hash of the encrypted payload). Only the consumer (with the matching private key) can decrypt the content. Third parties, including disputers, only see ciphertext.
What disputes use
In a DISPUTED transaction, the mediator gets:
- The on-chain attestation (proves provider claimed delivery).
- The Web Receipt (proves what was delivered).
- The disputer's
dispute.evidencefield.
Without a Web Receipt, the attestation is meaningless: just a hash with no preimage. Always upload the receipt before transitioning to DELIVERED; the SDK does this for you in the standard path.
Versioning
The version field allows the receipt schema to evolve. Today everything's 1.0. Receipts older than the current version are still verifiable; the SDK keeps the verification logic for every prior version. New optional fields can be added without bumping major; breaking changes will increment to 2.0.
What lives where
| Artifact | Where | Lifetime |
|---|---|---|
| Transaction state, amounts, parties | On-chain (Base L2) | Forever |
| Delivery attestation hash | On-chain (EAS) | Forever |
| Receipt JSON (input + output) | IPFS via Filebase/Pinata | Forever (pinned) |
Receipt's agirails.app shareable URL | agirails.app gateway | Available while agirails.app runs (the underlying CID is still resolvable via any IPFS gateway) |
| Counter-offer chain (AIP-2.1 negotiation) | Memory only (actp serve daemon) | Until daemon restart |
See also
- Receipts + discovery recipe: concrete walkthrough
- Dispute flow: what evidence the mediator looks at
- EAS schema: the attestation framework
- SDK reference: uploadReceipt