Canonical EIP-712 payment execution contract for OpenClaiming.
Implements:
- Delegated payments
- Trustline (line-based) accounting
- Deterministic EIP-712 verification (off-chain)
- Multi-line aggregation
OpenClaiming turns signed messages into portable, executable payment permissions.
OpenClaiming is a protocol for expressing signed claims that can be:
- verified across systems
- executed on-chain or off-chain
- reused as portable authorization primitives
Instead of sending transactions, users sign claims, which can later be executed.
Each payer has independent trustlines:
payer β line β max β spent β open/closed
Each claim:
- references a line
- defines a max
- defines allowed recipients
Execution enforces:
remaining = min(claim.max, line.max) - line.spent
Payment( address payer, address token, bytes32 recipientsHash, uint256 max, uint256 line, uint256 nbf, uint256 exp )
OpenClaiming supports both server-signed and wallet-signed claims, but is primarily designed for server-side signing.
- Build OpenClaiming JSON
- Convert to typed EIP-712 struct
- Compute hashes (e.g. recipientsHash)
- Sign using a server-controlled private key
In this model:
- Private keys are securely stored on backend systems
- The user does not rely on wallet UI to inspect signatures
- The application UI is responsible for transparency
This is the recommended and primary usage pattern.
- Build EIP-712 struct
- Present to user wallet (e.g. MetaMask)
- User signs via
eth_signTypedData_v4
Notes:
- Wallet support for complex structures (arrays, nested data) may vary
- Hardware wallets may not display full data correctly
- For better UX, additional fields (like full recipient arrays) may be included alongside hashes
- Recompute struct hash
- Recover signer using
ecrecover - Validate:
- signer matches payer / authority
- time constraints (
nbf,exp) - recipients hash matches provided recipients
- line limits and claim max
- Execute payment
OpenClaiming treats signatures as:
portable, machine-verifiable permissions
Not necessarily as user-facing wallet approvals.
- Most production flows will use server-side signing
- Wallet signing is supported but not required
- The trust model is application-driven, not wallet-driven
openClaiming.lineOpen(account, line, max);
openClaiming.executePayment(...)
- lineOpen(account, line, max)
- lineClose(account, line)
- lineAvailable(account, line)
- executePayment(...)
- executePaymentMulti(...)
- verifyPayment(...)
Requires:
token.approve(OpenClaiming, amount);
Then:
transferFrom(payer β recipient)
- token == address(0)
- must use msg.value
- cannot be delegated
Allows combining capacity across multiple claims:
- same payer
- same token
- same recipient
// SPDX-License-Identifier: MIT pragma solidity ^0.8.20;
import "./interfaces/IOpenClaiming.sol";
contract OpenClaimingConsumer {
address public constant OPENCLAIMING_ADDRESS = 0x0000000000000000000000000000000000000000;
IOpenClaiming public constant oc = IOpenClaiming(OPENCLAIMING_ADDRESS);
uint256 public constant PRICE = 1e18;
function purchase(
IOpenClaiming.Payment calldata payment,
address[] calldata recipients,
bytes calldata signature
) external payable {
bool ok = oc.verifyPayment(payment, signature);
require(ok, "invalid signature");
bool success = oc.executePayment{value: msg.value}(
payment,
recipients,
signature,
payment.line,
address(this),
PRICE
);
require(success, "payment failed");
_handlePurchase(payment.payer);
}
function _handlePurchase(address buyer) internal {
// application logic
}
}
import { ethers } from "ethers";
const domain = { name: "OpenClaiming.payments", version: "1", chainId: 1, };
const types = { Payment: [ { name: "payer", type: "address" }, { name: "token", type: "address" }, { name: "recipientsHash", type: "bytes32" }, { name: "max", type: "uint256" }, { name: "line", type: "uint256" }, { name: "nbf", type: "uint256" }, { name: "exp", type: "uint256" } ] };
const recipients = [ "0xRecipientAddress..." ];
const recipientsHash = ethers.keccak256( ethers.AbiCoder.defaultAbiCoder().encode(["address[]"], [recipients]) );
const value = { payer: signer.address, token: "0xTokenAddress...", recipientsHash, max: ethers.parseUnits("100", 18), line: 0, nbf: 0, exp: 0 };
const signature = await signer.signTypedData(domain, types, value);
- Only EOAs sign claims
- No JSON parsing on-chain
- Off-chain must prepare all data correctly
- line 0 is valid
- max = 0 means unlimited
- claim max is enforced relative to line usage
- does not parse JSON
- does not construct EIP-712 payloads
- does not sign messages
- does not store claims
All of that is handled off-chain.
OpenClaiming EVM provides:
- portable signed payments
- deterministic EIP-712 verification
- trustline-based accounting
- composable execution layer
It turns signatures into executable economic primitives.