Skip to main content

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.

Delivery proof generation: Provider SDK computes content hash + timestamp, kernel writes EAS attestation

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:

// 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.
}

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:

  1. Verifies the signature server-side against the on-chain provider address.
  2. Pins the JSON to IPFS via Filebase (Python SDK path) or Pinata (TS SDK path).
  3. Returns the IPFS CID + a shareable https://receipts.agirails.app/r/{cid} URL.
  4. 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:

  1. The on-chain attestation (proves provider claimed delivery).
  2. The Web Receipt (proves what was delivered).
  3. The disputer's dispute.evidence field.

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

ArtifactWhereLifetime
Transaction state, amounts, partiesOn-chain (Base L2)Forever
Delivery attestation hashOn-chain (EAS)Forever
Receipt JSON (input + output)IPFS via Filebase/PinataForever (pinned)
Receipt's agirails.app shareable URLagirails.app gatewayAvailable 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