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

# EIP-712 & Purchase Intents

> Canonical specification for the EIP-712 typed data structures used in Sumvin Purchase Intents

A <Tooltip headline="Stamped Mandate" tip="A signed authorisation a user grants for specific scoped actions — delivered on the wire as a PINT." cta="Glossary →" href="/glossary">Stamped Mandate</Tooltip> is the signed authorisation a user (or their agent) grants for specific scoped actions. On the wire it is delivered as a `PINT` — a Purchase Intent: an [EIP-712](https://eips.ethereum.org/EIPS/eip-712) typed-data message that carries the target wallet, a nonce, a scope envelope, a spend cap, and an expiry. Once signed, it is the cryptographic record of user consent — exchanged for verifiable JWTs via the SIS token service.

<Snippet file="product-term-disambiguation.mdx" />

This page defines the canonical EIP-712 specification for the `PINT` payload. Any client implementation must conform to these structures exactly for signatures to verify correctly.

## Why a PINT rather than a bearer token

A bearer token asserts "whoever holds this is allowed." A PINT asserts "this specific user signed this specific authorisation." That gives you four properties a bearer token can't:

* **Cryptographic consent.** The user's signature is over the full payload — the scopes, the spend cap, the expiry.
* **Independently verifiable.** A verifier can check the EIP-712 signature against the user's Safe without calling Sumvin, at the [Enhanced verification tier](/merchant/verification-tiers).
* **Scope-enveloped.** The signed payload carries [scopes](/identity/scopes) with per-scope parameters (amount caps, asset, chain, time window), so authorisation is fine-grained end-to-end.
* **Agent-compatible.** The same shape works whether the user signs with a passkey or an agent key signs on their behalf.

## When a partner creates one

Any time an action needs explicit user authorisation:

* Identity proof (KYC status, proof of personhood, age claim)
* On-chain spend via the user's Safe
* Creating an [<Tooltip headline="IPA" tip="Intelligent Purchase Authorisation — a scope-bound authorisation for an agent to execute purchases." cta="Glossary →" href="/glossary">IPA</Tooltip>](/concepts/ipas) for an Errand
* Linking a bank account or running a ramp session
* Card management

## PINT vs JWT

These are easy to confuse and different:

|                 | PINT                                        | PINT JWT                       |
| --------------- | ------------------------------------------- | ------------------------------ |
| **What it is**  | EIP-712 signed payload                      | ES256-signed JWT issued by SIS |
| **Who signs**   | The user (or their agent)                   | SIS                            |
| **Direction**   | Input to token exchange                     | Output to send to verifiers    |
| **Consumed by** | SIS token exchange, Enhanced-tier verifiers | All verifiers                  |

The PINT is the signed input. The JWT is the issued output. A verifier receiving `x-sumvin-pint-token` is consuming the JWT; an Enhanced-tier verifier additionally consumes the original PINT via `X-Pint-Signature` + `X-Pint-Payload`.

## Domain Separator

The EIP-712 domain separator identifies the signing context and prevents cross-protocol replay attacks:

```json theme={null}
{
  "name": "Sumvin Purchase Intent",
  "version": "1",
  "chainId": 1329,
  "verifyingContract": "0x..."
}
```

| Field               | Type      | Description                            |
| ------------------- | --------- | -------------------------------------- |
| `name`              | `string`  | Always `"Sumvin Purchase Intent"`      |
| `version`           | `string`  | Schema version. Currently `"1"`        |
| `chainId`           | `uint256` | Target chain ID (e.g., `1329` for Sei) |
| `verifyingContract` | `address` | The user's Safe Smart Account address  |

The `verifyingContract` is the user's Safe wallet address — the same address that appears in the `wallet` field of the PINT payload.

## PurchaseIntent Type

The structured data that the user signs:

```json theme={null}
{
  "PurchaseIntent": [
    { "name": "wallet", "type": "address" },
    { "name": "nonce", "type": "uint256" },
    { "name": "statement", "type": "string" },
    { "name": "scopes", "type": "string[]" },
    { "name": "resources", "type": "string[]" },
    { "name": "maxAmount", "type": "uint256" },
    { "name": "maxAmountToken", "type": "address" },
    { "name": "expiresAt", "type": "uint256" }
  ]
}
```

### Field Reference

| Field            | Type       | Required | Description                                                                                               |
| ---------------- | ---------- | -------- | --------------------------------------------------------------------------------------------------------- |
| `wallet`         | `address`  | Yes      | The user's Safe Smart Account address                                                                     |
| `nonce`          | `uint256`  | Yes      | Monotonically increasing per-wallet counter for replay prevention                                         |
| `statement`      | `string`   | Yes      | Human-readable summary of the intent (shown to user at signing time)                                      |
| `scopes`         | `string[]` | Yes      | Authorised [scopes](/identity/scopes) in SRI format (e.g., `["sr:us:pint:identity:proof_of_personhood"]`) |
| `resources`      | `string[]` | Yes      | SRI URIs for associated resources                                                                         |
| `maxAmount`      | `uint256`  | Yes      | Spend cap in token units. `0` means no limit                                                              |
| `maxAmountToken` | `address`  | Yes      | Token denomination for `maxAmount`. Zero address (`0x000...`) means USD-equivalent                        |
| `expiresAt`      | `uint256`  | Yes      | Unix timestamp after which the PINT is no longer valid                                                    |

## Example PINT Payload

```json theme={null}
{
  "wallet": "0xE23c9A70BC749EBddd8c78a864fd911D04E9e992",
  "nonce": 42,
  "statement": "Purchase authorization for partner X",
  "scopes": ["sr:us:pint:identity:proof_of_personhood", "sr:us:pint:personalization:read"],
  "resources": ["sr:us:pint:abc123"],
  "maxAmount": 0,
  "maxAmountToken": "0x0000000000000000000000000000000000000000",
  "expiresAt": 1740000000
}
```

## Constructing the EIP-712 Hash

To sign a PINT, you construct the EIP-712 typed data hash following the standard:

```typescript theme={null}
import { hashTypedData } from "viem";

const domain = {
  name: "Sumvin Purchase Intent",
  version: "1",
  chainId: 1329n,
  verifyingContract: "0xE23c9A70BC749EBddd8c78a864fd911D04E9e992",
} as const;

const types = {
  PurchaseIntent: [
    { name: "wallet", type: "address" },
    { name: "nonce", type: "uint256" },
    { name: "statement", type: "string" },
    { name: "scopes", type: "string[]" },
    { name: "resources", type: "string[]" },
    { name: "maxAmount", type: "uint256" },
    { name: "maxAmountToken", type: "address" },
    { name: "expiresAt", type: "uint256" },
  ],
} as const;

const message = {
  wallet: "0xE23c9A70BC749EBddd8c78a864fd911D04E9e992",
  nonce: 42n,
  statement: "Purchase authorization for partner X",
  scopes: ["sr:us:pint:identity:proof_of_personhood", "sr:us:pint:personalization:read"],
  resources: ["sr:us:pint:abc123"],
  maxAmount: 0n,
  maxAmountToken: "0x0000000000000000000000000000000000000000",
  expiresAt: 1740000000n,
};

const hash = hashTypedData({ domain, types, primaryType: "PurchaseIntent", message });
```

<Info>
  This example uses [viem](https://viem.sh/), but any EIP-712 compliant library will produce the same hash. See [Signing a PINT](/identity/signing) for the full signing flow.
</Info>

## Versioning

The EIP-712 specification is versioned via the domain separator's `version` field. The current version is `"1"`. When the type structure changes, the version will increment, and both old and new versions will be supported during a migration window.

Schema validation is enforced server-side at the SIS token exchange endpoint — if the PINT payload doesn't match the canonical type structure for the declared version, the exchange returns `400 Bad Request` with error code `PINT-400-001`.

## Where PINTs appear

* [Payment request links](/products/payment-links/payment-request-links) — user-originated payment URLs backed by a signed PINT.
* [Global x402 acceptance](/products/payment-links/global-x402-acceptance) — every identity's persistent x402 endpoint, authorised per-settlement against a PINT.
