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

# Signed Offers & Receipts

> Sign offers on 402 responses and receipts on 200 responses, producing cryptographic proof-of-interaction artifacts that clients can use for reputation, auditing, or dispute resolution.

The Offer & Receipt extension adds cryptographic proof-of-interaction to x402 payment flows. When enabled, your server automatically signs an **offer** on every `402` response (committing to payment terms) and a **receipt** on every `200` response (confirming service delivery). No changes to your business logic.

## Why Enable Offer & Receipt Signing?

Signed offers and receipts are portable, verifiable artifacts that any third party can check. They enable:

* **Reputation systems** — Clients can attach receipts to on-chain attestations as proof they actually paid for and received a service. This is the "Verified Purchase" equivalent for the open web.
* **Dispute resolution** — Offers prove the server committed to specific terms; receipts prove delivery. If either party disputes a transaction, the signed artifacts provide evidence.
* **Auditing** — Receipts create a verifiable trail of service delivery without exposing transaction details (the transaction hash is optional).
* **Client confidence** — Services with verifiable proof-of-interaction build stronger trust signals, making new clients more likely to use the service.

## Prerequisites

* An existing x402 resource server (or a new Express.js project)
* Node.js 18+
* A facilitator URL (see [Quickstart for Sellers](/getting-started/quickstart-for-sellers))

## Installation

```bash theme={null}
npm install @x402/express @x402/extensions @x402/evm @x402/core viem
```

## Signing Formats

The extension supports two signature formats. Choose based on your key management setup:

| Format      | Key Type                                          | Identity                                         | Best For                                                                                                                                            |
| ----------- | ------------------------------------------------- | ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| **EIP-712** | secp256k1 (Ethereum)                              | `did:pkh` (address recovered from signature)     | Wallet-based signing. Simpler setup, especially with managed wallet providers.                                                                      |
| **JWS**     | Any asymmetric key (EC P-256, Ed25519, secp256k1) | `did:web` (resolved via `/.well-known/did.json`) | Server-side signing with KMS/HSM. Also supports Solana keys (Ed25519), so if your infrastructure is Solana-native, JWS may be the more natural fit. |

Both formats produce equivalent proof artifacts. Clients and verifiers handle both transparently.

## Quick Start: EIP-712 with Environment Variables

This example uses EIP-712 signing with a raw private key from an environment variable. This is the simplest way to get started.

> **Not for Production:** Storing private keys in environment variables is acceptable for local development and testing. For production deployments, use a key management service (KMS), hardware security module (HSM), or a managed wallet provider. See [Production Key Management](#production-key-management) below.

> **Signing Key ≠ Payment Address:** The signing key used for offers and receipts should be a dedicated signing key, not the wallet that receives payments (`payTo`). Separating signing from payment receipt limits exposure if the signing key is compromised.

### Environment Variables

Create a `.env` file:

```bash theme={null}
# Wallet address that receives payments
EVM_ADDRESS=0xYourPaymentWalletAddress

# Private key for signing offers and receipts (EIP-712)
# This should be a DEDICATED SIGNING KEY, not the payment wallet's key
# For production deployments, do not store private keys in an environment variable
SIGNING_PRIVATE_KEY=0xYourDedicatedSigningPrivateKey

# x402 facilitator URL
FACILITATOR_URL=https://facilitator.x402.org
```

### Server Setup (EIP-712)

```typescript theme={null}
import { config } from "dotenv";
import express from "express";
import { paymentMiddleware, x402ResourceServer } from "@x402/express";
import { ExactEvmScheme } from "@x402/evm/exact/server";
import { HTTPFacilitatorClient } from "@x402/core/server";
import {
  createOfferReceiptExtension,
  createEIP712OfferReceiptIssuer,
  declareOfferReceiptExtension,
} from "@x402/extensions/offer-receipt";
import { privateKeyToAccount } from "viem/accounts";

config();

const evmAddress = process.env.EVM_ADDRESS as `0x${string}`;
const signingPrivateKey = process.env.SIGNING_PRIVATE_KEY as `0x${string}`; // not for production
const facilitatorUrl = process.env.FACILITATOR_URL!;

// Create EIP-712 signer from the dedicated signing key
const signingAccount = privateKeyToAccount(signingPrivateKey);
const kid = `did:pkh:eip155:1:${signingAccount.address}#key-1`;

const offerReceiptIssuer = createEIP712OfferReceiptIssuer(
  kid,
  signingAccount.signTypedData.bind(signingAccount),
);

// Set up the resource server with the extension
const facilitatorClient = new HTTPFacilitatorClient({ url: facilitatorUrl });
const resourceServer = new x402ResourceServer(facilitatorClient)
  .register("eip155:84532", new ExactEvmScheme())
  .registerExtension(createOfferReceiptExtension(offerReceiptIssuer));

const app = express();

// Configure payment routes with offer-receipt enabled
app.use(
  paymentMiddleware(
    {
      "GET /api/data": {
        accepts: [
          {
            scheme: "exact",
            price: "$0.001",
            network: "eip155:84532",
            payTo: evmAddress, // Payment goes here (different from signing key)
          },
        ],
        description: "Premium data endpoint",
        mimeType: "application/json",
        extensions: {
          ...declareOfferReceiptExtension({ includeTxHash: false }),
        },
      },
    },
    resourceServer,
  ),
);

// Your business logic — unchanged
app.get("/api/data", (req, res) => {
  res.json({ data: "your premium content" });
});

app.listen(4021, () => {
  console.log("Server listening on http://localhost:4021");
  console.log("Offer-receipt extension enabled (EIP-712)");
});
```

### What Happens Automatically

Once configured, the extension hooks into the x402 payment flow:

1. **On `402` responses**: The extension signs an offer for each entry in `accepts[]` and includes them in the response's `extensions` field. Each offer contains the payment terms (`scheme`, `network`, `amount`, `payTo`) and a `validUntil` timestamp.

2. **On `200` responses** (after successful payment): The extension signs a receipt containing the `resourceUrl`, `payer` address, `network`, and `issuedAt` timestamp. The receipt is included in the `PAYMENT-RESPONSE` header's `extensions` field.

No changes to your route handlers are needed. The extension is composable middleware.

## Alternative: JWS Signing with `did:web`

JWS signing uses a `did:web` identifier, which means your server must host a DID document at `/.well-known/did.json`. Clients and verifiers resolve this document to find your public key so they can verify the signature.

JWS supports a wider range of key types than EIP-712 (secp256k1 only), including secp256r1 (EC P-256), Ed25519, and secp256k1 (ES256K). If your infrastructure is enterprise-oriented or Solana-native (Ed25519), JWS lets you use your existing key infrastructure.

### Environment Variables

```bash theme={null}
EVM_ADDRESS=0xYourPaymentWalletAddress
FACILITATOR_URL=https://facilitator.x402.org

# Base64-encoded PKCS#8 private key (EC P-256)
# For production deployments, do not store private keys in an environment variable
SIGNING_PRIVATE_KEY=base64EncodedPrivateKey

# Your server's domain (URL-encoded for did:web)
# e.g., "api.example.com" or "localhost%3A4021" for local dev
SERVER_DOMAIN=api.example.com
```

### Server Setup (JWS)

```typescript theme={null}
import * as crypto from "crypto";
import {
  createOfferReceiptExtension,
  createJWSOfferReceiptIssuer,
  declareOfferReceiptExtension,
  type JWSSigner,
} from "@x402/extensions/offer-receipt";

const serverDomain = process.env.SERVER_DOMAIN!;
const signingPrivateKey = process.env.SIGNING_PRIVATE_KEY!; // not for production

const did = `did:web:${serverDomain}`;
const kid = `${did}#key-1`;

// Create JWS signer from PKCS#8 private key
const privateKeyPem = `-----BEGIN PRIVATE KEY-----\n${signingPrivateKey}\n-----END PRIVATE KEY-----`;
const keyObject = crypto.createPrivateKey(privateKeyPem);
const publicKeyJwk = keyObject.export({ format: "jwk" });
delete (publicKeyJwk as Record<string, unknown>).d; // Remove private component

const jwsSigner: JWSSigner = {
  kid,
  format: "jws",
  algorithm: "ES256",
  async sign(payload: Uint8Array): Promise<string> {
    const sign = crypto.createSign("SHA256");
    sign.update(payload);
    const signature = sign.sign(privateKeyPem);
    return Buffer.from(derToRaw(signature)).toString("base64url");
  },
};

const offerReceiptIssuer = createJWSOfferReceiptIssuer(kid, jwsSigner);

// Register with x402ResourceServer the same way as the EIP-712 example:
// resourceServer.registerExtension(createOfferReceiptExtension(offerReceiptIssuer));
```

### Hosting the DID Document

For JWS verification, clients resolve your `did:web` to find the public key. Serve the DID document at `/.well-known/did.json`:

```typescript theme={null}
app.get("/.well-known/did.json", (req, res) => {
  res.setHeader("Content-Type", "application/did+json");
  res.json({
    "@context": [
      "https://www.w3.org/ns/did/v1",
      "https://w3id.org/security/suites/jws-2020/v1",
    ],
    id: did,
    verificationMethod: [
      {
        id: kid,
        type: "JsonWebKey2020",
        controller: did,
        publicKeyJwk,
      },
    ],
    assertionMethod: [kid],
  });
});
```

## Configuration

The `declareOfferReceiptExtension` function accepts an optional configuration object:

```typescript theme={null}
declareOfferReceiptExtension({
  // Include the blockchain transaction hash in receipts.
  // Default: false (for privacy — the payer address is still included).
  // Set to true if verifiability is more important than privacy.
  includeTxHash: false,

  // How long offers remain valid, in seconds.
  // Default: 300 (5 minutes). Falls back to the route's maxTimeoutSeconds.
  offerValiditySeconds: 300,
});
```

Configuration is per-route — different endpoints can have different settings.

## What Gets Signed

### Offer Payload

Each offer is signed when the server returns a `402 Payment Required` response:

| Field         | Description                                     |
| ------------- | ----------------------------------------------- |
| `resourceUrl` | The URL the client is requesting                |
| `offerType`   | The payment scheme (e.g., `exact`)              |
| `network`     | The blockchain network (e.g., `eip155:84532`)   |
| `amount`      | The payment amount in the token's smallest unit |
| `payTo`       | The server's payment address                    |
| `validUntil`  | Unix timestamp after which the offer expires    |

### Receipt Payload

Each receipt is signed when the server returns a `200` response after successful payment:

| Field         | Description                                                                          |
| ------------- | ------------------------------------------------------------------------------------ |
| `resourceUrl` | The URL the client requested                                                         |
| `payer`       | The client's wallet address (from the payment)                                       |
| `network`     | The blockchain network used for payment                                              |
| `issuedAt`    | Unix timestamp when the receipt was issued                                           |
| `txHash`      | *(optional)* The blockchain transaction hash, included only if `includeTxHash: true` |

Both payloads are signed using the format configured on the server (EIP-712 or JWS). The signed artifacts are self-contained — a verifier only needs the artifact and the signer's public key to verify.

## Production Key Management

> The examples above use environment variables for signing keys. This is fine for development but not for production. Private keys in environment variables can leak through process inspection, logging, crash dumps, and container metadata endpoints.

For production, use a signing backend that keeps keys in secure hardware or managed infrastructure. The extension's signer interface is pluggable — you only need to implement the `sign()` function (for JWS) or `signTypedData()` function (for EIP-712) using your provider's SDK. The `OfferReceiptIssuer` interface handles the rest.

When using a managed wallet provider, you won't have access to the raw private key. Instead, you call the provider's signing API. Here's what the EIP-712 setup looks like with a server wallet (conceptual example):

```typescript theme={null}
import {
  createOfferReceiptExtension,
  createEIP712OfferReceiptIssuer,
} from "@x402/extensions/offer-receipt";

// The provider's SDK gives you a signTypedData function
// that calls their API — the private key never leaves their infrastructure
const signerAddress = "0xYourServerWalletAddress";
const kid = `did:pkh:eip155:1:${signerAddress}#key-1`;

const offerReceiptIssuer = createEIP712OfferReceiptIssuer(kid, async (params) => {
  // Call your wallet provider's signing API
  return await yourWalletProvider.signTypedData({
    domain: params.domain,
    types: params.types,
    primaryType: params.primaryType,
    message: params.message,
  });
});

// Register as usual
resourceServer.registerExtension(createOfferReceiptExtension(offerReceiptIssuer));
```

The key difference from the environment variable example: you never construct a `privateKeyToAccount` — instead, you pass a function that delegates signing to the provider's API. Any managed wallet provider that supports `signTypedData` (for EIP-712) or raw signing (for JWS) works as a drop-in replacement.

## Binding Your Signing Key to Your Service Identity

Signing offers and receipts is only half the story. For verifiers to trust that your signatures are legitimate, they need to confirm that your signing key is authorized to act on behalf of your service's identity (`did:web:yourdomain.com`).

### DID Document (`did.json`)

If you're using JWS signing, you're already hosting a DID document at `/.well-known/did.json` (see [JWS setup above](#hosting-the-did-document)). This document declares which keys are authorized for your `did:web` identity. Verifiers resolve your DID and check that the signing key is listed in `verificationMethod`.

If you're using EIP-712 signing, you can host a `did.json` as well — list your EIP-712 signing address as a `verificationMethod` so verifiers can confirm the key is authorized for your domain.

This is a W3C standard mechanism and is sufficient for many use cases. However, the DID document is mutable — if you remove the key later, verifiers checking at that point won't find it.

### Additional Binding Mechanisms

For production services that need stronger guarantees — immutable on-chain attestations, DNS-based verification, temporal anchoring, or key lifecycle management (expiration, rotation, revocation) — ecosystem partners offer additional trust layers on top of `did.json`. See the [Infrastructure & Tooling category](https://www.x402.org/ecosystem?filter=ecosystem-infrastructure) on the ecosystem page for reputation and identity services that integrate with the offer-receipt extension.

## Client-Side: Extracting Offers and Receipts

The `@x402/extensions` package provides client utilities for extracting and verifying the signed artifacts your server produces.

### Extract Offers from a `402` Response

```typescript theme={null}
import {
  extractOffersFromPaymentRequired,
  decodeSignedOffers,
  verifyOfferSignatureJWS,
  verifyOfferSignatureEIP712,
  isJWSSignedOffer,
} from "@x402/extensions/offer-receipt";

// After receiving a 402 response:
const paymentRequiredBody = await response.json();
const signedOffers = extractOffersFromPaymentRequired(paymentRequiredBody);
const decodedOffers = decodeSignedOffers(signedOffers);

// Verify an offer signature
for (const decoded of decodedOffers) {
  if (isJWSSignedOffer(decoded.signedOffer)) {
    await verifyOfferSignatureJWS(decoded.signedOffer);
  } else {
    await verifyOfferSignatureEIP712(decoded.signedOffer);
  }
}
```

### Extract a Receipt from a `200` Response

```typescript theme={null}
import {
  extractReceiptFromResponse,
  verifyReceiptMatchesOffer,
  verifyReceiptSignatureJWS,
  verifyReceiptSignatureEIP712,
  isJWSSignedReceipt,
} from "@x402/extensions/offer-receipt";

// After a successful payment response:
const signedReceipt = extractReceiptFromResponse(paidResponse);

// Verify the receipt signature
if (isJWSSignedReceipt(signedReceipt)) {
  await verifyReceiptSignatureJWS(signedReceipt);
} else {
  await verifyReceiptSignatureEIP712(signedReceipt);
}

// Verify the receipt matches the offer you accepted
const verified = verifyReceiptMatchesOffer(
  signedReceipt,
  selectedOffer,
  [yourWalletAddress],
);
```

`verifyReceiptMatchesOffer` checks that:

* `resourceUrl` matches the offer
* `network` matches the offer
* `payer` matches one of your wallet addresses
* `issuedAt` is recent (within 1 hour by default)

### What Can Clients Do with These Artifacts?

Signed offers and receipts are portable, verifiable artifacts. Clients can:

* **Attach them to reputation attestations** as proof-of-interaction (e.g., "Verified Purchase" reviews)
* **Store them for auditing** — receipts create a verifiable trail of service delivery
* **Use them in dispute resolution** — offers prove the server committed to terms; receipts prove delivery
* **Share them with aggregators** — trust scoring engines can verify the signatures independently

Ecosystem partners (see the [Infrastructure & Tooling category](https://www.x402.org/ecosystem?filter=ecosystem-infrastructure) on the ecosystem page) build on these artifacts to provide reputation systems, trust scoring, and other value-added services.

## Working Examples

Complete working examples are available in the x402 repository:

* [Server Example (Express.js)](https://github.com/x402-foundation/x402/tree/main/examples/typescript/servers/offer-receipt) — Resource server with offer-receipt enabled, showing both EIP-712 and JWS configurations
* [Client Example](https://github.com/x402-foundation/x402/tree/main/examples/typescript/clients/offer-receipt) — Complete client flow: offer extraction, payment, receipt capture, and verification

## Further Reading

* [Extensions Overview](./overview) — How the x402 extension system works
* [Offer & Receipt Extension Specification](https://github.com/x402-foundation/x402/blob/main/specs/extensions/extension-offer-and-receipt.md) — Full protocol spec with payload schemas, EIP-712 types, verification rules, and wire format examples
* [@x402/extensions package](https://github.com/x402-foundation/x402/tree/main/typescript/packages/extensions/src/offer-receipt) — TypeScript implementation source
* [SDK Features](/sdk-features) — Extension support across TypeScript, Go, and Python
