> ## Documentation Index
> Fetch the complete documentation index at: https://docs.agentchain.cloud/llms.txt
> Use this file to discover all available pages before exploring further.

# Receipts

> Understand the v2 receipt lifecycle, EIP-712 signature structure, and verification methods.

## What is a receipt?

A receipt is a **cryptographically signed, gateway-issued document** that records the outcome of a policy guard request. It is the trust artifact of the AgentChain system — independently verifiable without a network call.

## Receipt lifecycle

```mermaid theme={null}
flowchart LR
    A[issued] --> B[executed]
    B --> C[anchored]
    A --> D[denied]
    A --> E[approval_required]
    A --> F[expired]

    style A fill:#0a0a0b,stroke:#5ba3b5,stroke-width:2px,color:#fafafa
    style B fill:#0a0a0b,stroke:#818cf8,stroke-width:2px,color:#fafafa
    style C fill:#0a0a0b,stroke:#10B981,stroke-width:2px,color:#fafafa
    style D fill:#0a0a0b,stroke:#fbbf24,stroke-width:2px,color:#fafafa
    style E fill:#0a0a0b,stroke:#d7b94d,stroke-width:2px,color:#fafafa
    style F fill:#0a0a0b,stroke:#fbbf24,stroke-width:2px,color:#fafafa
```

| State               | Terminal? | Meaning                                                     |
| ------------------- | --------- | ----------------------------------------------------------- |
| `issued`            | No        | Guard evaluated, decision signed, awaiting client execution |
| `executed`          | No        | Client reported `txHash` via `reportExecution()`            |
| `anchored`          | Yes       | `receiptHash` written on-chain to `ReceiptAnchor`           |
| `denied`            | Yes       | Policy rejected — no execution possible                     |
| `approval_required` | No        | Held in approval queue pending operator decision            |
| `expired`           | Yes       | `decisionExpiresAt` passed before execution was reported    |

## Receipt fields

```typescript theme={null}
type EvmGuardReceiptV2 = {
  receiptId: string;           // UUID
  state: 'issued' | 'executed' | 'denied' | 'approval_required' | 'expired' | 'anchored';
  decision: 'allow' | 'deny' | 'approval_required';

  // Policy binding
  policyId: string;
  chainId: number;
  policyHash: string;          // keccak256(JCS(policy body at version))
  policyVersion: number;
  policySource: 'gateway' | 'anchored' | 'onchain_registry';

  // Transaction intent (what was guarded)
  from: string;
  to: string;
  data: string;
  value: string;
  calldataHash: string;        // keccak256(data)
  simulationHash: string;      // keccak256(blockNum + returnData) — ZERO_B32 in MVP

  // Timing
  issuedAt: string;            // ISO 8601
  decisionExpiresAt: string;   // ISO 8601 — MUST send before this (default: issuedAt + 300s)
  receiptExpiresAt: string;    // ISO 8601 (0 = never)

  // Signature (EIP-712)
  receiptHash: string;         // EIP-712 typed data digest
  signature: string;           // 65-byte ECDSA (hex)
  signerAddress: string;       // recovering address
  signerKeyId: string;         // key rotation handle

  // Deny / approval
  reasonCodes: string[];       // e.g. ['VALUE_EXCEEDED']
  approvalId: string | null;   // present when decision = approval_required

  // Execution (filled after reportExecution)
  executionTxHash: string | null;  // self-reported — NOT covered by ECDSA signature (see below)
  executionChainId: number | null;
  executionReportedAt: string | null;

  // Anchor (filled after on-chain anchoring)
  anchorTxHash: string | null; // tx that wrote receiptHash to ReceiptAnchor
};
```

> **Verifier Note — `executionTxHash` is self-reported**
>
> The EIP-712 ECDSA signature covers the receipt body **at issue time**, when `executionTxHash = bytes32(0)`.
> After `reportExecution()`, the `executionTxHash` field is updated in the database but the **original
> signature is not re-issued**. Verifiers must use the original `receiptHash` (computed at issue time)
> for ECDSA verification — not a re-hashed post-execution body.

> **Deny Receipts are Signed**
>
> `deny` and `approval_required` receipts are signed with the same ECDSA key as `allow` receipts.
> **A signed deny receipt is a tamper-evident proof that the system evaluated and blocked the
> transaction.** The `decision` field is included in the signed EIP-712 struct — it cannot be altered.

## Signature scheme

Receipts are signed with **EIP-712** (`keccak256(0x1901 ‖ domainSeparator ‖ structHash)`).

```typescript theme={null}
const ReceiptDomain = {
  name: "AgentChain Receipt",
  version: "2",
  chainId: <number>,                        // chain the receipt authorizes
  verifyingContract: "0x36cbAE566545e8df3ad15C171a7840266526E28F", // SignerAnchor (Base Sepolia)
};
```

The `chainId` in the domain separator **binds the signature to a specific chain**. A receipt signed for Base Sepolia (84532) cannot be replayed on Base mainnet (8453) — the domain hash will differ.

## Local verification (no network)

```typescript theme={null}
import { AgentChain } from '@humbleaf/agentchain-sdk';

const ac = new AgentChain({ apiKey: '...' });
const valid = ac.receipts.verify(
  receipt.receiptHash as `0x${string}`,
  receipt.signature as `0x${string}`,
  receipt.signerAddress as `0x${string}`,
);
```

Uses `@agentchain/core` `verifyReceiptSignature` — pure ECDSA recover, no HTTP.

## On-chain verification (trustless)

```typescript theme={null}
const ac = new AgentChain({
  apiKey: '...',
  signerAnchorAddress: '0x36cbAE566545e8df3ad15C171a7840266526E28F',
  signerAnchorChainId: 84532,
  signerAnchorRpcUrl: 'https://sepolia.base.org',
});

const result = await ac.receipts.verifyWithAnchor(receipt);
```

`verifyWithAnchor` performs two independent checks:

1. **ECDSA verify** — `signerAddress` correctly recovers from `receiptHash` + `signature`
2. **On-chain check** — `SignerAnchor.isAuthorized(signerAddress)` returns `true`

```typescript theme={null}
// VerifyReceiptResult — discriminated union on valid + onChainVerified
type VerifyReceiptResult =
  | { valid: true;  onChainVerified: true  }                         // ECDSA + on-chain: fully trustless
  | { valid: true;  onChainVerified: false; reasons: string[] }      // ECDSA ok, on-chain degraded
  | { valid: false; onChainVerified: boolean; reasons: string[] };   // ECDSA failed

// reasons include: 'signature_mismatch' | 'signer_revoked' | 'signer_anchor_unavailable' | 'signer_anchor_rpc_error'
```

The SDK degrades gracefully (IC-30): if the RPC call fails, `anchorState` is `'unavailable'` and ECDSA verification alone is used. Never a silent pass.

## Remote verification

```typescript theme={null}
const result = await ac.receipts.verifyRemote(receipt);
if (!result.valid) {
  console.error('Invalid:', result.reasons);
}
```

The gateway checks signature, expiry, state, and `SignerAnchor` revocation status.

## Fetch by ID

```typescript theme={null}
const stored = await ac.receipts.get(receiptId);
```

## Report execution

After `guardTransaction` returns `allow`, call this after you send the transaction:

```typescript theme={null}
const updated = await ac.evm.reportExecution(receipt.receiptId, txHash);
// updated.state === 'executed'
```

`guardAndExecute` calls this automatically.

## `decisionExpiresAt`

The gateway sets `decisionExpiresAt` to `issuedAt + 300s` (configurable). If your execution logic takes more than 5 minutes, call `guardTransaction` again for a fresh receipt.

The SDK throws `AgentChainExpiredDecision` if `decisionExpiresAt < now` when `guardAndExecute` tries to send.

> **Policy revocation and the 5-minute window**
>
> A receipt issued just before a policy is revoked remains valid for up to **300 seconds** (5 minutes).
> This is by design — in-flight, already-authorized transactions must be allowed to complete.
> After `decisionExpiresAt`, `reportExecution` returns `410 DECISION_EXPIRED`.
> The window cannot be shortened without breaking in-flight transactions.

## Anchoring

When a policy has `anchoring = "receipt_hash_onchain"`, the gateway's `ReceiptAnchorJob` batch-writes the `receiptHash` to `ReceiptAnchor.sol` on the declared chain. The receipt transitions from `executed` → `anchored` after 2 confirmations.

**`ReceiptAnchor` on Base Sepolia:** `0x1F98D953785047f25a4886D12D103F4D67F1D8B3`

```typescript theme={null}
// Poll anchor status
const status = await ac.receipts.getAnchorStatus(receiptId);
// status.state — 'pending' | 'anchored'
// status.anchorTxHash — string | null
```
