# Stripe x402 Machine Payments Profile

**Status:** Draft
**Since:** v0.10.11
**Package:** `@peac/rails-stripe`
**Function:** `fromCryptoPaymentIntent()`

## Abstract

This profile describes how to normalize Stripe crypto payment intents into PEAC PaymentEvidence for x402 machine-to-machine payment flows. It covers the mapping between Stripe's crypto payment data and PEAC's payment evidence structure.

## Use Case

Machine-to-machine payments settled through Stripe's x402 crypto integration. An AI agent pays for an API call using USDC on Base via Stripe, and the provider issues a PEAC receipt with crypto payment evidence.

```text
Agent -> Stripe (x402) -> Provider API -> PEAC Receipt
         USDC/Base                        PaymentEvidence
```

## Mapping

### Input: StripeCryptoPaymentIntent

| Field       | Type   | Required | Description                                             |
| ----------- | ------ | -------- | ------------------------------------------------------- |
| `id`        | string | Yes      | Stripe payment intent ID (e.g., `pi_3QxYz...`)          |
| `amount`    | number | Yes      | Amount in smallest currency unit (cents)                |
| `currency`  | string | Yes      | Fiat currency code, lowercase ISO 4217 (e.g., `usd`)    |
| `asset`     | string | Yes      | Crypto asset ticker (e.g., `usdc`, `eth`)               |
| `network`   | string | Yes      | Network identifier, CAIP-2 format (e.g., `eip155:8453`) |
| `tx_hash`   | string | No       | On-chain transaction hash                               |
| `recipient` | string | No       | Recipient wallet address                                |
| `customer`  | string | No       | Stripe customer ID                                      |
| `metadata`  | object | No       | Stripe metadata (key-value strings)                     |

### Output: PaymentEvidence

| Field       | Value                           | Notes                                     |
| ----------- | ------------------------------- | ----------------------------------------- |
| `rail`      | `"stripe"`                      | Stripe is the payment facilitator         |
| `reference` | `intent.id`                     | Stripe payment intent ID                  |
| `amount`    | `intent.amount`                 | Passed through unchanged                  |
| `currency`  | `intent.currency.toUpperCase()` | Fiat denomination (e.g., `"USD"`)         |
| `asset`     | `intent.asset.toUpperCase()`    | Crypto token (e.g., `"USDC"`)             |
| `env`       | `"live"` or `"test"`            | Caller-specified                          |
| `network`   | `intent.network`                | CAIP-2 identifier (e.g., `"eip155:8453"`) |
| `evidence`  | object                          | See below                                 |

### Evidence Object

| Field               | Source                       | Presence    |
| ------------------- | ---------------------------- | ----------- |
| `payment_intent_id` | `intent.id`                  | Always      |
| `asset`             | `intent.asset.toUpperCase()` | Always      |
| `network`           | `intent.network`             | Always      |
| `tx_hash`           | `intent.tx_hash`             | If provided |
| `recipient`         | `intent.recipient`           | If provided |
| `customer_id`       | `intent.customer`            | If provided |
| `metadata`          | `intent.metadata`            | If provided |

### Commerce Event (Optional)

When issuing a Wire 0.2 receipt with the `org.peacprotocol/commerce` extension,
the commerce extension supports an optional `event` field that records the
commerce lifecycle phase: `authorization`, `capture`, `settlement`, `refund`,
`void`, or `chargeback`. This field is observational metadata only; it does not
encode settlement finality or enforce lifecycle ordering. Omitting `event` is
valid and preserves backward compatibility.

## Key Distinction: Fiat vs Crypto

|            | `fromPaymentIntent()` (fiat)       | `fromCryptoPaymentIntent()` (crypto)            |
| ---------- | ---------------------------------- | ----------------------------------------------- |
| `asset`    | Same as `currency` (e.g., `"USD"`) | Crypto token (e.g., `"USDC"`)                   |
| `network`  | Not set                            | CAIP-2 ID (e.g., `"eip155:8453"`)               |
| `evidence` | `payment_intent_id`, `customer_id` | Adds `asset`, `network`, `tx_hash`, `recipient` |

## Supported Networks

Common CAIP-2 identifiers for Stripe crypto payments:

| Network                | CAIP-2 ID        | Common Assets   |
| ---------------------- | ---------------- | --------------- |
| Ethereum mainnet       | `eip155:1`       | ETH, USDC, USDT |
| Base mainnet           | `eip155:8453`    | ETH, USDC       |
| Base Sepolia (testnet) | `eip155:84532`   | ETH, USDC       |
| Solana mainnet         | `solana:mainnet` | SOL, USDC       |

## Validation Rules

1. `id` MUST be a non-empty string
2. `amount` MUST be a non-negative number
3. `currency` MUST match `/^[a-z]{3}$/` (lowercase ISO 4217)
4. `asset` MUST be a non-empty string
5. `network` MUST be CAIP-2 format: `/^[a-z][a-z0-9-]{2,31}:[a-zA-Z0-9]{1,64}$/`
6. `tx_hash`, if present, MUST be a non-empty string (opaque: no chain-specific validation)
7. `recipient`, if present, MUST be a non-empty string (opaque: no chain-specific validation)

Violations throw with descriptive error messages. No silent coercion.

**Note on tx_hash and recipient:** These fields are treated as opaque strings.
Chain-specific validation (e.g., 0x-prefix for EVM, base58 for Solana) is intentionally
not performed: the adapter normalizes, it does not interpret chain semantics.

## Denomination Semantics

| Field      | Unit                               | Example       | Notes                        |
| ---------- | ---------------------------------- | ------------- | ---------------------------- |
| `amount`   | Smallest fiat unit                 | `500` = $5.00 | Cents for USD, pence for GBP |
| `currency` | ISO 4217 (uppercase in output)     | `"USD"`       | Fiat denomination, not token |
| `asset`    | Token ticker (uppercase in output) | `"USDC"`      | Crypto settlement asset      |

`amount` and `currency` describe the **fiat denomination**. `asset` describes the
**crypto settlement token**. These may differ (e.g., 500 USD settled in USDC).
The adapter does not perform exchange rate conversion.

## Privacy Defaults

`fromCryptoPaymentIntent()` enforces privacy by default:

| Field         | Default  | Opt-in flag         | Risk                                    |
| ------------- | -------- | ------------------- | --------------------------------------- |
| `customer_id` | Excluded | `includeCustomerId` | Links receipts to Stripe customer       |
| `metadata`    | Excluded | `metadataPolicy`    | May contain PII (emails, IPs)           |
| `tx_hash`     | Included | n/a                 | On-chain public, enables graph analysis |
| `recipient`   | Included | n/a                 | On-chain public, enables graph analysis |

### Metadata Policy

The `metadataPolicy` option controls how Stripe metadata is included in evidence.
It takes precedence over the boolean `includeMetadata` flag.

| Policy          | Behavior                                          |
| --------------- | ------------------------------------------------- |
| `'omit'`        | No metadata in evidence (default)                 |
| `'passthrough'` | Include all metadata with bounds enforcement      |
| `'allowlist'`   | Include only keys listed in `metadataAllowedKeys` |

When metadata is included (`passthrough` or `allowlist`), the following bounds
are enforced:

- Maximum 20 entries (excess silently dropped)
- Maximum key length: 40 characters (truncated)
- Maximum value length: 500 characters (truncated)
- Invisible Unicode characters stripped (zero-width, direction overrides, BOM)

```typescript
// Default: metadata excluded
fromCryptoPaymentIntent(intent);

// Passthrough: include all metadata with bounds enforcement
fromCryptoPaymentIntent(intent, { metadataPolicy: 'passthrough' });

// Allowlist: include only specified keys
fromCryptoPaymentIntent(intent, {
  metadataPolicy: 'allowlist',
  metadataAllowedKeys: ['agent_id', 'tool_call'],
});

// Legacy boolean (equivalent to metadataPolicy: 'passthrough')
fromCryptoPaymentIntent(intent, { includeMetadata: true });

// Include customer ID as well
fromCryptoPaymentIntent(intent, {
  includeCustomerId: true,
  metadataPolicy: 'allowlist',
  metadataAllowedKeys: ['agent_id'],
});
```

For privacy-critical deployments, also omit `recipient` from the input intent
to prevent transaction graph analysis.

**Design note: why `passthrough` instead of `hash`.** An earlier design
considered a `'hash'` policy that would SHA-256 metadata values before
inclusion. This was rejected because hashing low-entropy strings (short
agent IDs, tool names, boolean flags) is trivially reversible by
exhaustive guessing. A hash that looks private but isn't is worse than
plaintext that's clearly visible: it creates a false sense of
anonymization. Use `'allowlist'` to include only the keys you need, or
`'omit'` to exclude metadata entirely.

## Trust Model

A PEAC receipt with Stripe evidence is an **issuer attestation**: the receipt
signer asserts that payment occurred. Offline verification confirms the receipt's
**integrity and origin** (Ed25519 signature check), not on-chain settlement.

To independently verify that an on-chain transaction exists, use the `tx_hash`
and `network` fields with a block explorer or RPC endpoint. The PEAC adapter
does not perform on-chain verification.

## Quick Demo

Normalize a crypto payment, issue a PEAC receipt, and verify offline: in one command:

```bash
pnpm --filter @peac/example-stripe-x402-crypto demo
```

See `examples/stripe-x402-crypto/demo.ts` for the full source. Runs in under 5 seconds,
no network or Stripe API needed.

## Conformance Vectors

See `specs/conformance/fixtures/stripe-crypto/` for test vectors:

- `minimal-crypto-intent.json`: required fields only
- `full-crypto-intent.json`: all fields populated
- `missing-required-fields.json`: validation error cases (8 cases)

Vectors are executed by the conformance runner at
`packages/rails/stripe/tests/conformance.test.ts`.

## Example

```typescript
import { fromCryptoPaymentIntent } from '@peac/rails-stripe';
import { issue } from '@peac/protocol';
import { generateKeypair } from '@peac/crypto';

// Normalize Stripe crypto payment intent
const payment = fromCryptoPaymentIntent({
  id: 'pi_3QxYz1234567890',
  amount: 500,
  currency: 'usd',
  asset: 'usdc',
  network: 'eip155:8453',
  tx_hash: '0xabc123...',
});

// Issue a PEAC receipt with the payment evidence
const { privateKey } = await generateKeypair();
const { jws } = await issue({
  iss: 'https://api.example.com',
  kind: 'evidence',
  type: 'org.peacprotocol/payment',
  pillars: ['commerce'],
  extensions: {
    'org.peacprotocol/commerce': {
      payment_rail: payment.rail,
      amount_minor: String(payment.amount),
      currency: payment.currency,
      reference: payment.reference,
      asset: payment.asset,
      network: payment.network,
    },
  },
  privateKey,
  kid: '2026-02-13',
});

console.log('Receipt JWS:', jws);
// Verify offline with receipt.jws + public key
```
