Skip to main content
A Purchase Intent (PINT) is an EIP-712 typed-data message that a user (or their agent) signs to declare what they are authorising. A PINT 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. This page defines the canonical EIP-712 specification. 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.
  • Scope-enveloped. The signed payload carries 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 for perpetual search
  • Linking a bank account or running a ramp session
  • Card management

PINT vs JWT

These are easy to confuse and different:
PINTPINT JWT
What it isEIP-712 signed payloadES256-signed JWT issued by SIS
Who signsThe user (or their agent)SIS
DirectionInput to token exchangeOutput to send to verifiers
Consumed bySIS token exchange, Enhanced-tier verifiersAll 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:
{
  "name": "Sumvin Purchase Intent",
  "version": "1",
  "chainId": 1329,
  "verifyingContract": "0x..."
}
FieldTypeDescription
namestringAlways "Sumvin Purchase Intent"
versionstringSchema version. Currently "1"
chainIduint256Target chain ID (e.g., 1329 for Sei)
verifyingContractaddressThe 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:
{
  "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

FieldTypeRequiredDescription
walletaddressYesThe user’s Safe Smart Account address
nonceuint256YesMonotonically increasing per-wallet counter for replay prevention
statementstringYesHuman-readable summary of the intent (shown to user at signing time)
scopesstring[]YesAuthorised scopes in SRI format (e.g., ["sr:us:pint:identity:proof_of_personhood"])
resourcesstring[]YesSRI URIs for associated resources
maxAmountuint256YesSpend cap in token units. 0 means no limit
maxAmountTokenaddressYesToken denomination for maxAmount. Zero address (0x000...) means USD-equivalent
expiresAtuint256YesUnix timestamp after which the PINT is no longer valid

Example PINT Payload

{
  "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:
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 });
This example uses viem, but any EIP-712 compliant library will produce the same hash. See Signing a PINT for the full signing flow.

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