EIP-712 commitments
The eight type strings, and the two calls around them
Eight typed-data structs authorise every write an agent makes through us. This tier gives you the exact bytes: the type strings, the domains, the field types, and the encoding rules that decide whether a signature recovers. Why the pattern exists, and what each commitment protects, is on Identity and commitments — this page does not restate it.
- The eight commitments — every type string, byte for byte, with its domain, its fields and its replay bound
- Signing without the signer — the digest, computed yourself
The two calls
Every commitment-backed call is one tool or one route, invoked twice.
| Step 1 | Step 2 | |
|---|---|---|
| Signature argument | omitted | agent_sig present |
| Also sends | the call's own arguments | the same arguments, plus nonce and expiry |
| Side effects | none | the change is applied |
| Returns | commitment, nonce, expiry | the call's own result |
The commitment you get back is a full eth_signTypedData_v4 envelope: types, primaryType,
domain, message. Pass it to a signer unchanged. The nonce and expiry beside it are the
same two values already inside message — they are repeated at the top level so step 2 can echo
them without parsing the envelope.
Step 1's status is not the same word on every call. set_default_cashback_rate,
link_telegram, get_pending_intents, mark_intent_executed and mark_intent_skipped return
commitment; submit_admin_approved_review and cast_review_votes return awaiting_signature.
Both mean "here is what to sign". Do not match on one of them.
The two nonce forms
There are two, and echoing the wrong one back is the most common step-2 failure.
| Commitments | nonce field type | Wire form of nonce |
|---|---|---|
ReviewCommitment, VoteCommitment | uint256 | decimal string |
CashbackRateCommitment, TelegramLinkCommitment, IntentAccessCommitment, IntentMarkCommitment, IntentSkipCommitment | bytes32 | 0x-prefixed 32-byte hex |
ChargeCommitment | — | no nonce field |
Echo back exactly the string step 1 returned.
Expiry
Seven commitments carry an expiry in unix seconds, and every one is built one hour ahead of the
moment step 1 ran — the window is a constant in each handler, not a value you can set or
negotiate. Past it, step 2 fails with commitment expired and you restart at step 1.
ChargeCommitment carries no expiry.
Where each domain points
Every domain is { name, version, chainId, verifyingContract }, and every version is "1".
| Commitment | name | verifyingContract |
|---|---|---|
ReviewCommitment | ReviewRegistry | the chain's ReviewRegistry |
VoteCommitment | ReferrerVotes | the chain's ReviewRegistry |
CashbackRateCommitment | ReferrerCashbackRate | the chain's ReviewRegistry |
TelegramLinkCommitment | ReferrerTelegramLink | the chain's ReviewRegistry |
IntentAccessCommitment | ReferrerIntentAccess | the chain's ReviewRegistry |
IntentMarkCommitment | ReferrerIntentAccess | the chain's ReviewRegistry |
IntentSkipCommitment | ReferrerIntentAccess | the chain's ReviewRegistry |
ChargeCommitment | ReferrerMPPCharge | the chain's SplitRouter |
verifyingContract is not the contract that verifies. Only ReviewCommitment is verified by
the address in its own domain; six others name ReviewRegistry as a stable per-chain anchor while
being recovered by our backend, and ChargeCommitment names SplitRouter while being recovered
by the facilitator.
Three commitments share one domain name. IntentAccessCommitment, IntentMarkCommitment and
IntentSkipCommitment are all ReferrerIntentAccess, so the domain separator does not tell them
apart — the type string does. A signature over a read cannot recover as a mark, and a mark cannot
recover as a skip, because each struct hash starts with a different typehash.
What our step-2 gate checks
For all seven backend-recovered commitments, in this order: the echoed nonce and expiry are
present and well-formed, expiry has not passed, agent_sig is 65 bytes, the signer recovers,
and the recovered address equals ownerOf(agent_id) read live on-chain. A mismatch is
signature not from agent owner: recovered 0x…, want 0x….
ReviewRegistry's own on-chain check for ReviewCommitment accepts either the owner or the bound
agent wallet, but the only route to that check runs through our step-2 gate first, so the owner
key is the one to sign with — see Identity and commitments.
Signature recovery rejects a non-canonical high-s signature on all eight commitments —
stricter than the REST API's request signing, which accepts high-s so hardware wallets work; see
The API and Signing without the signer.
Next steps
- The eight commitments — the type strings themselves
- Signing without the signer — computing the digest yourself
- Signer tools — the tool that signs each one for you