# `X402.Extensions.OfferReceipt`
[🔗](https://github.com/cardotrejos/x402/blob/v0.9.0/lib/x402/extensions/offer_receipt.ex#L1)

Offer-and-receipt extension for x402: signed offers and signed receipts.

Implements the
[offer-and-receipt extension](https://github.com/x402-foundation/x402/blob/main/specs/extensions/extension-offer-and-receipt.md):
the resource server cryptographically commits to the payment terms it
advertises (a **signed offer** placed under
`extensions["offer-receipt"].info.offers[]` of the payment requirements)
and, after successful payment and delivery, confirms the transaction (a
**signed receipt** under `extensions["offer-receipt"].info.receipt` of the
settlement response). Both artifacts are portable, independently
verifiable, and identical for x402 v1 and v2.

Two signature formats are supported, per the specification (§3.1):

  * `"eip712"` — an EIP-712 typed-data signature with the fixed domain
    `{name: "x402 offer" | "x402 receipt", version: "1", chainId: 1}`
    (chain-agnostic by design, §3.2) and the canonical `Offer` / `Receipt`
    types from §4.3 and §5.3. Signing goes through the `X402.Signer`
    behaviour; verification recovers the signer address. Requires the
    optional `ex_keccak` (and, for verification, `ex_secp256k1`)
    dependencies.
  * `"jws"` — a compact JWS (`header.payload.signature`) with `ES256K` or
    `EdDSA`, implemented with OTP `:crypto` by
    `X402.Extensions.OfferReceipt.JWS`. The protected header carries the
    mandatory `alg` and `kid` (a DID URL) fields (§3.3); payloads are
    JCS-canonicalized (§10).

## Server side: issuing offers and receipts

    {:ok, signer} = X402.Signer.LocalKey.new(private_key)

    {:ok, payload} =
      X402.Extensions.OfferReceipt.offer_payload(
        resource_url: "https://api.example.com/premium-data",
        scheme: "exact",
        network: "eip155:8453",
        asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
        pay_to: "0x209693Bc6afc0C5328bA36FaF03C514EF312287C",
        amount: "10000",
        valid_until: System.os_time(:second) + 300
      )

    {:ok, offer} = X402.Extensions.OfferReceipt.sign_offer(payload, signer, accept_index: 0)

    extensions = %{"offer-receipt" => X402.Extensions.OfferReceipt.build_extension([offer])}

## Client side: verifying

    {:ok, [offer]} = X402.Extensions.OfferReceipt.fetch_offers(payment_required)

    {:ok, %{signer: signer_address, payload: payload}} =
      X402.Extensions.OfferReceipt.verify_offer(offer)

A valid signature only proves *which key* signed — it does not prove the
key was **authorized** for the offered resource. Verifiers must apply an
authorization policy (§4.5.1); the simplest is checking the recovered
signer against the offer's `payTo` address, which `verify_offer/2` supports
through the `:expected_signer` option.

## Boundaries

  * JWS verification takes an explicit `:public_key` — this library never
    resolves `kid` DID URLs (no network access); resolve the key yourself
    and check its authorization per §4.5.1.
  * The JWS algorithms are limited to what OTP `:crypto` provides:
    `ES256K` and `EdDSA` (both spec-named algorithms are covered).

# `extract_payload`
*since 0.6.0* 

```elixir
@spec extract_payload(envelope()) :: {:ok, payload()} | {:error, verify_error()}
```

Extracts the payload from a signed envelope **without verifying it**.

For `"eip712"` the transmitted `payload` is returned; for `"jws"` the JWS
payload segment is decoded. Use `verify_offer/2` / `verify_receipt/2` for
verified reads.

## Examples

    iex> X402.Extensions.OfferReceipt.extract_payload(%{
    ...>   "format" => "eip712",
    ...>   "payload" => %{"version" => 1},
    ...>   "signature" => "0xsig"
    ...> })
    {:ok, %{"version" => 1}}

    iex> X402.Extensions.OfferReceipt.extract_payload(%{"format" => "carrier-pigeon"})
    {:error, {:unsupported_format, "carrier-pigeon"}}

# `offer_digest`
*since 0.6.0* 

```elixir
@spec offer_digest(payload()) ::
  {:ok, &lt;&lt;_::256&gt;&gt;} | {:error, payload_error() | :missing_dependency}
```

Computes the EIP-712 digest of an offer payload (§4.3).

Returns `keccak256(0x19 0x01 || domainSeparator || structHash)` where the
domain is `{name: "x402 offer", version: "1", chainId: 1}`. An absent
`validUntil` is hashed as `0`. Requires the optional `ex_keccak`
dependency.

# `offer_payload`
*since 0.6.0* 

```elixir
@spec offer_payload(keyword()) ::
  {:ok, payload()} | {:error, {:unknown_network, String.t()}}
```

Builds an offer payload (§4.2) in wire shape.

The `:network` is normalized to CAIP-2 with `to_caip2/1`, as the
specification requires for offer payloads. `version` is always the current
payload schema version (`1`).

## Options

* `:resource_url` (`t:String.t/0`) - Required. The paid resource URL.

* `:scheme` (`t:String.t/0`) - Required. Payment scheme identifier (e.g. `"exact"`).

* `:network` (`t:String.t/0`) - Required. Network identifier — CAIP-2 or an x402 v1 name (converted via `to_caip2/1`).

* `:asset` (`t:String.t/0`) - Required. Token contract address or `"native"`.

* `:pay_to` (`t:String.t/0`) - Required. Recipient wallet address.

* `:amount` - Required. Required payment amount in atomic units (encoded as a string).

* `:valid_until` (`t:non_neg_integer/0`) - Unix timestamp (seconds) when the offer expires. Omit for no expiry.

## Examples

    iex> X402.Extensions.OfferReceipt.offer_payload(
    ...>   resource_url: "https://api.example.com/premium-data",
    ...>   scheme: "exact",
    ...>   network: "base",
    ...>   asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
    ...>   pay_to: "0x209693Bc6afc0C5328bA36FaF03C514EF312287C",
    ...>   amount: 10_000,
    ...>   valid_until: 1_703_123_516
    ...> )
    {:ok,
     %{
       "version" => 1,
       "resourceUrl" => "https://api.example.com/premium-data",
       "scheme" => "exact",
       "network" => "eip155:8453",
       "asset" => "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
       "payTo" => "0x209693Bc6afc0C5328bA36FaF03C514EF312287C",
       "amount" => "10000",
       "validUntil" => 1703123516
     }}

    iex> X402.Extensions.OfferReceipt.offer_payload(
    ...>   resource_url: "https://a.example",
    ...>   scheme: "exact",
    ...>   network: "unknown-net",
    ...>   asset: "native",
    ...>   pay_to: "0x209693Bc6afc0C5328bA36FaF03C514EF312287C",
    ...>   amount: "1"
    ...> )
    {:error, {:unknown_network, "unknown-net"}}

# `sign_offer`
*since 0.6.0* 

```elixir
@spec sign_offer(payload(), X402.Signer.t(), keyword()) ::
  {:ok, envelope()} | {:error, verify_error() | term()}
```

Signs an offer payload as an EIP-712 artifact through an `X402.Signer`.

Computes the EIP-712 digest with the fixed offer domain
(`name: "x402 offer"`, `version: "1"`, `chainId: 1`, §3.2) and the
canonical `Offer` type (§4.3), signs it through `signer`, and returns the
transmitted envelope

    %{"format" => "eip712", "payload" => payload, "signature" => "0x..."}

Per §4.3, an absent `validUntil` is signed **and transmitted** as `0`.
Requires the optional `ex_keccak` dependency (`{:error,
:missing_dependency}` without it).

## Options

* `:accept_index` (`t:non_neg_integer/0`) - Index into `accepts[]` this offer corresponds to. An unsigned
  convenience field (§4.1.1) — clients match offers by payload fields.

# `sign_offer_jws`
*since 0.6.0* 

```elixir
@spec sign_offer_jws(
  payload(),
  keyword()
) ::
  {:ok, envelope()}
  | {:error, X402.Extensions.OfferReceipt.JWS.sign_error() | payload_error()}
```

Signs an offer payload as a compact JWS artifact.

The payload travels inside the JWS, so the envelope omits `payload`
(§3.1.1). Optional payload fields stay omitted (no zero-filling — that
rule is EIP-712 specific).

`key_opts` are the `X402.Extensions.OfferReceipt.JWS.sign/2` options
(`:alg`, `:kid`, `:key`) plus the `:accept_index` envelope option.

## Examples

    {:ok, offer} =
      X402.Extensions.OfferReceipt.sign_offer_jws(payload,
        alg: "EdDSA",
        kid: "did:web:api.example.com#key-1",
        key: ed25519_seed,
        accept_index: 0
      )

# `verify_offer`
*since 0.6.0* 

```elixir
@spec verify_offer(
  envelope(),
  keyword()
) :: {:ok, verification()} | {:error, verify_error()}
```

Verifies a signed offer envelope (§4.5).

For `"eip712"` artifacts, recomputes the EIP-712 digest from the payload
**exactly as transmitted** and recovers the signer address (requires the
optional `ex_secp256k1` / `ex_keccak` dependencies). For `"jws"`
artifacts, verifies the compact JWS against the `:public_key` option.

Returns `{:ok, %{format: ..., payload: ...}}` with `:signer` (EIP-712, the
recovered address) or `:header` (JWS, the protected header with `alg` and
`kid`).

> #### Signature validity is not authorization {: .warning}
>
> A valid signature proves which key signed, not that the key was
> authorized for `payload.resourceUrl` (§4.5.1). Check the signer against
> your authorization policy — for the common `payTo`-signs deployment,
> pass `expected_signer: payload["payTo"]`.

## Options

* `:expected_signer` (`t:String.t/0`) - EIP-712 only: EVM address the recovered signer must equal
  (case-insensitive), e.g. the offer's `payTo`. When the recovered signer
  differs, verification fails with `{:error, :unauthorized_signer}`.

* `:public_key` (`t:String.t/0`) - JWS only (required for `"jws"` artifacts): raw public key bytes — a
  32-byte Ed25519 key for `EdDSA`, a SEC1 secp256k1 point for `ES256K`.

* `:algs` - JWS only: accepted algorithms. The default value is `["ES256K", "EdDSA"]`.

# `receipt_digest`
*since 0.6.0* 

```elixir
@spec receipt_digest(payload()) ::
  {:ok, &lt;&lt;_::256&gt;&gt;} | {:error, payload_error() | :missing_dependency}
```

Computes the EIP-712 digest of a receipt payload (§5.3).

The domain is `{name: "x402 receipt", version: "1", chainId: 1}`; an
absent `transaction` is hashed as `""`. Requires the optional `ex_keccak`
dependency.

# `receipt_payload`
*since 0.6.0* 

```elixir
@spec receipt_payload(keyword()) ::
  {:ok, payload()} | {:error, {:unknown_network, String.t()}}
```

Builds a receipt payload (§5.2) in wire shape.

Receipts are privacy-minimal by default: `transaction` is only included
when passed. `:issued_at` defaults to the current Unix time.

## Options

* `:resource_url` (`t:String.t/0`) - Required. The paid resource URL.

* `:network` (`t:String.t/0`) - Required. Network identifier — CAIP-2 or an x402 v1 name (converted via `to_caip2/1`).

* `:payer` (`t:String.t/0`) - Required. Payer identifier (commonly a wallet address).

* `:issued_at` (`t:non_neg_integer/0`) - Unix timestamp (seconds) the receipt was issued. Defaults to the current time.

* `:transaction` (`t:String.t/0`) - Blockchain transaction hash. Optional — receipts are privacy-minimal
  by default; include it when verifiability matters more than privacy.

## Examples

    iex> X402.Extensions.OfferReceipt.receipt_payload(
    ...>   resource_url: "https://api.example.com/premium-data",
    ...>   network: "eip155:8453",
    ...>   payer: "0x857b06519E91e3A54538791bDbb0E22373e36b66",
    ...>   issued_at: 1_703_123_456
    ...> )
    {:ok,
     %{
       "version" => 1,
       "network" => "eip155:8453",
       "resourceUrl" => "https://api.example.com/premium-data",
       "payer" => "0x857b06519E91e3A54538791bDbb0E22373e36b66",
       "issuedAt" => 1703123456
     }}

# `sign_receipt`
*since 0.6.0* 

```elixir
@spec sign_receipt(payload(), X402.Signer.t()) :: {:ok, envelope()} | {:error, term()}
```

Signs a receipt payload as an EIP-712 artifact through an `X402.Signer`.

Uses the fixed receipt domain (`name: "x402 receipt"`, `version: "1"`,
`chainId: 1`, §3.2) and the canonical `Receipt` type (§5.3). Per §5.3, an
absent `transaction` is signed **and transmitted** as `""`.

# `sign_receipt_jws`
*since 0.6.0* 

```elixir
@spec sign_receipt_jws(
  payload(),
  keyword()
) ::
  {:ok, envelope()}
  | {:error, X402.Extensions.OfferReceipt.JWS.sign_error() | payload_error()}
```

Signs a receipt payload as a compact JWS artifact.

See `sign_offer_jws/2`; `key_opts` are `:alg`, `:kid`, and `:key`.

# `verify_receipt`
*since 0.6.0* 

```elixir
@spec verify_receipt(
  envelope(),
  keyword()
) :: {:ok, verification()} | {:error, verify_error()}
```

Verifies a signed receipt envelope (§5.5).

Same contract as `verify_offer/2`, with the receipt domain and types.
`issuedAt` policy checks (freshness) and on-chain `transaction` checks are
the caller's responsibility.

# `build_extension`
*since 0.6.0* 

```elixir
@spec build_extension([envelope()]) :: t()
```

Builds the `extensions["offer-receipt"]` value for payment requirements.

Takes the signed offer envelopes for the response's `accepts[]` entries
and returns the `%{"info" => %{"offers" => offers}, "schema" => schema}`
declaration (§4.1, §6.1/§6.3). All offers must share one signature format
— the specification's schemas are format-specific and servers use one
format consistently (§6). Raises `ArgumentError` for empty, mixed-format,
or structurally invalid offers (programmer errors).

# `build_receipt_extension`
*since 0.6.0* 

```elixir
@spec build_receipt_extension(envelope()) :: t()
```

Builds the `extensions["offer-receipt"]` value for a settlement response.

Takes the signed receipt envelope and returns the
`%{"info" => %{"receipt" => receipt}, "schema" => schema}` declaration
(§5.1, §6.5/§6.7). Raises `ArgumentError` for a structurally invalid
receipt (programmer error).

# `fetch_offers`
*since 0.6.0* 

```elixir
@spec fetch_offers(map()) ::
  {:ok, [envelope()]}
  | {:error, :extension_not_present | {:invalid_extension, term()}}
```

Fetches and validates the signed offers from a payment-required map.

Accepts either the full decoded `PAYMENT-REQUIRED` map (looks under
`"extensions"`) or the extensions map itself. Validation is fail-closed:
every offer envelope must be structurally valid per §3.1.1
(`"eip712"` offers carry a payload and a 65-byte hex signature; `"jws"`
offers carry a three-part compact JWS and **no** payload).

## Examples

    iex> X402.Extensions.OfferReceipt.fetch_offers(%{"accepts" => []})
    {:error, :extension_not_present}

# `fetch_receipt`
*since 0.6.0* 

```elixir
@spec fetch_receipt(map()) ::
  {:ok, envelope()}
  | {:error, :extension_not_present | {:invalid_extension, term()}}
```

Fetches and validates the signed receipt from a settlement response map.

Accepts either the full decoded settlement response (looks under
`"extensions"`) or the extensions map itself.

# `validate_offer`
*since 0.6.0* 

```elixir
@spec validate_offer(envelope()) :: :ok | {:error, verify_error()}
```

Validates the structure of a signed offer envelope (§3.1.1, §4.2).

## Examples

    iex> X402.Extensions.OfferReceipt.validate_offer(%{
    ...>   "format" => "jws",
    ...>   "signature" => "eyJhbGciOiJFUzI1NksiLCJraWQiOiJrIn0.eyJ2ZXJzaW9uIjoxfQ.c2ln"
    ...> })
    :ok

    iex> X402.Extensions.OfferReceipt.validate_offer(%{
    ...>   "format" => "jws",
    ...>   "payload" => %{"version" => 1},
    ...>   "signature" => "a.b.c"
    ...> })
    {:error, {:invalid_field, "payload"}}

# `validate_receipt`
*since 0.6.0* 

```elixir
@spec validate_receipt(envelope()) :: :ok | {:error, verify_error()}
```

Validates the structure of a signed receipt envelope (§3.1.1, §5.2).

# `envelope`

```elixir
@type envelope() :: %{optional(String.t()) =&gt; term()}
```

A signed offer or receipt envelope in wire shape (string keys).

# `payload`

```elixir
@type payload() :: %{optional(String.t()) =&gt; term()}
```

An offer or receipt payload in wire shape (string keys).

# `payload_error`

```elixir
@type payload_error() ::
  {:missing_field, String.t()}
  | {:invalid_field, String.t()}
  | {:unsupported_payload_version, term()}
  | {:unknown_network, String.t()}
```

# `t`

```elixir
@type t() :: %{optional(String.t()) =&gt; map()}
```

A built `extensions["offer-receipt"]` value (`info` + `schema`).

# `verification`

```elixir
@type verification() :: %{
  optional(:signer) =&gt; String.t(),
  optional(:header) =&gt; map(),
  format: String.t(),
  payload: payload()
}
```

The result of a successful verification.

# `verify_error`

```elixir
@type verify_error() ::
  payload_error()
  | X402.Extensions.OfferReceipt.JWS.verify_error()
  | {:unsupported_format, term()}
  | :invalid_envelope
  | :missing_public_key
  | :unauthorized_signer
  | :missing_dependency
  | :invalid_signature
```

# `to_caip2`
*since 0.6.0* 

```elixir
@spec to_caip2(String.t()) ::
  {:ok, String.t()} | {:error, {:unknown_network, String.t()}}
```

Converts a network identifier to CAIP-2 format.

Offer and receipt payloads must carry CAIP-2 identifiers even in x402 v1
flows (§4.2, §5.2). Strings that already contain a `:` are passed through;
x402 v1 names are mapped (`"base"` → `"eip155:8453"`, `"solana"` → its
CAIP-2 chain reference); anything else is `{:error, {:unknown_network,
network}}`.

## Examples

    iex> X402.Extensions.OfferReceipt.to_caip2("eip155:8453")
    {:ok, "eip155:8453"}

    iex> X402.Extensions.OfferReceipt.to_caip2("base-sepolia")
    {:ok, "eip155:84532"}

    iex> X402.Extensions.OfferReceipt.to_caip2("solana")
    {:ok, "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"}

    iex> X402.Extensions.OfferReceipt.to_caip2("mystery-chain")
    {:error, {:unknown_network, "mystery-chain"}}

---

*Consult [api-reference.md](api-reference.md) for complete listing*
