Signer tools
The thirteen tools that hold your key
Thirteen tools on referrer-signer, the server that runs on your machine over stdio and holds the
private key. Eight sign an EIP-712 commitment and return a signature. Five send a transaction;
none carries a commitment, and the four that call a contract spend your own key's gas. Nothing
here calls our backend except pay_for_product, and even that decodes the ref link locally rather
than asking us to resolve it.
Configuration
| Variable | Needed by | Notes |
|---|---|---|
AGENT_KEYS | every tool | Comma-separated agentId:0xkey. Validated at boot |
AGENT_PRIVATE_KEY | every tool | Single-key fallback for any agent id with no AGENT_KEYS entry |
BASE_RPC_URL | the five transaction tools | Must point at the chain the tool will use |
CHAIN_ID | the five transaction tools | Defaults to Base Sepolia when unset. Overridden per call by chain_id where the tool accepts one |
REVIEW_REGISTRY_ADDRESS | publish_escrow_review, update_review_content_onchain | |
COMMISSION_ESCROW_ADDRESS | publish_escrow_review, bind_escrow, trigger_settle_fallback | |
REFERRER_AGENT_MCP_URL | pay_for_product with inline votes | Points at the referrer-agent MCP endpoint |
At least one of AGENT_KEYS and AGENT_PRIVATE_KEY must be set or the process exits at boot.
Key selection is by agent id: an exact AGENT_KEYS entry first, then the fallback. The fallback
answers for any agent id, which is why a wrong-agent transaction is caught by the contract
rather than by the key lookup.
The seven commitment-object signing tools
Seven of the eight signing tools take the same shape — one commitment object, passed
exactly as the referrer-agent step-1 response returned it, including its types and
primaryType — and all seven return the same shape: a single signature, 0x-prefixed
65-byte hex, ready to pass back as agent_sig. sign_charge_commitment is the eighth and is
the exception on both counts; it has its own section below.
Do not strip or reshape the object. The signer recomputes the digest from domain and message
and ignores types/primaryType, but it must still accept them, and the key it signs with is
selected from message.agentId.
Returns, all seven
| Field | Notes |
|---|---|
signature | 0x-prefixed 65-byte hex. Pass it as agent_sig in the caller's step 2 |
Errors, all seven
| Message | Cause |
|---|---|
no key for agent N — not in AGENT_KEYS and no AGENT_PRIVATE_KEY fallback | No key resolves for message.agentId |
compute digest: invalid agentId: "…" | A message field is not the decimal or hex form the type expects |
compute digest: invalid expiry: "…" | expiry is not a decimal string |
compute digest: invalid nonce: must be 0x-prefixed 32-byte hex | A 32-byte-nonce commitment given a decimal nonce |
compute digest: invalid nonce: "…" | A decimal-nonce commitment given hex |
compute digest: invalid productId / invalid contentHash / invalid votesHash | A bytes32 message field is malformed |
compute digest: cashbackOfCommissionBps out of uint16 range: N | Rate above 65535 |
sign: … | The key rejected the digest |
There is no signature verification here and no policy check: whatever commitment you hand it, it
signs. The gate is on the receiving end — and it is the identity owner's key that gate wants
on all seven, because the backend's step-2 check recovers the signer and compares it against
ownerOf. A signature from the bound agent wallet is rejected even where the contract itself
would have accepted it; see Identity and commitments.
The message shape differs per commitment, and so does the form of nonce. Each tool below
shows the one it must receive. domain is elided in the examples — pass through whatever step 1
returned.
sign_review_commitment
Signs a ReviewCommitment. Call it between submit_admin_approved_review steps 1 and 2.
{ "commitment": { "primaryType": "ReviewCommitment", "types": { }, "domain": { },
"message": { "agentId": "7", "merchantId": "3", "productId": "0x1f…",
"contentHash": "0x9c…", "nonce": "834…", "expiry": "1785…" } } }nonce is a decimal uint256 string here.
sign_vote_commitment
Signs a VoteCommitment. Call it between cast_review_votes steps 1 and 2.
{ "commitment": { "primaryType": "VoteCommitment", "types": { }, "domain": { },
"message": { "agentId": "7", "escrowId": "88", "votesHash": "0x4b…",
"nonce": "1290…", "expiry": "1785…" } } }nonce is a decimal uint256 string here too. votesHash commits to the whole votes array, so
it cannot change between the caller's two steps.
sign_cashback_rate_commitment
Signs a CashbackRateCommitment. Call it between set_default_cashback_rate steps 1 and 2.
{ "commitment": { "primaryType": "CashbackRateCommitment", "types": { }, "domain": { },
"message": { "agentId": "7", "cashbackOfCommissionBps": "1500",
"nonce": "0x6d…", "expiry": "1785…" } } }From here down, nonce is 0x-prefixed 32-byte hex, not a decimal.
sign_telegram_link_commitment
Signs a TelegramLinkCommitment. Call it between link_telegram steps 1 and 2.
{ "commitment": { "primaryType": "TelegramLinkCommitment", "types": { }, "domain": { },
"message": { "agentId": "7", "nonce": "0x6d…", "expiry": "1785…" } } }The caller burns this nonce on use, so a signature is good for exactly one deep link.
sign_intent_access_commitment
Signs an IntentAccessCommitment. Call it between get_pending_intents steps 1 and 2.
{ "commitment": { "primaryType": "IntentAccessCommitment", "types": { }, "domain": { },
"message": { "agentId": "7", "nonce": "0x6d…", "expiry": "1785…" } } }This is the one signature that authorises a read, and its nonce is not burned — treat it as a short-lived read token.
sign_intent_mark_commitment
Signs an IntentMarkCommitment. Call it between mark_intent_executed steps 1 and 2.
{ "commitment": { "primaryType": "IntentMarkCommitment", "types": { }, "domain": { },
"message": { "agentId": "7", "intentId": "itn_9f3c…", "result": "escrow:88",
"nonce": "0x6d…", "expiry": "1785…" } } }intentId and result are both inside the signed struct, so this signature authorises exactly
one intent recorded with exactly one result. result may be the empty string.
sign_intent_skip_commitment
Signs an IntentSkipCommitment. Call it between mark_intent_skipped steps 1 and 2.
{ "commitment": { "primaryType": "IntentSkipCommitment", "types": { }, "domain": { },
"message": { "agentId": "7", "intentId": "itn_9f3c…",
"reason": "live price above the confirmed cap",
"nonce": "0x6d…", "expiry": "1785…" } } }Same shape as the mark commitment with reason in place of result — and unlike result, the
caller requires it to be non-empty.
sign_charge_commitment
Signs a ChargeCommitment with the buyer wallet that made the charge transfer and owns the buyer
agent.
This one takes five explicit arguments rather than a commitment object, because the values
come from the payment you just made rather than from a step-1 response. The resulting signature
goes to POST /mpp/charge as buyerSignature, not back into an MCP tool.
{ "txHash": "0x…", "attributionToken": "eyJ…", "buyerAgentId": "7" }…plus chainId (a number) and splitRouterAddr, both required and both rejected at zero.
Parameters
| Name | Type | Notes |
|---|---|---|
txHash | string | 0x-prefixed 32-byte charge transaction hash. Becomes the commitment's paymentRef |
attributionToken | string | The base64url signed token, hashed into the commitment |
buyerAgentId | string | Decimal. Also selects the signing key |
chainId | integer | Required and non-zero. The Base chain id, for the EIP-712 domain |
splitRouterAddr | string | Required and non-zero. The Base SplitRouter address, the domain's verifying contract |
The payer address in the commitment is derived from the key, not passed — the charge endpoint requires the recovered signer to equal the on-chain transfer sender.
Returns
| Field | Notes |
|---|---|
signature | 0x-prefixed 65-byte hex |
Errors
| Message | Cause |
|---|---|
chainId is required and must be nonzero… | Zero or omitted. The message names the retired tempoChainId field explicitly |
splitRouterAddr is required and must be a nonzero address… | Empty or zero. The message names the retired splitRouterMppAddr field |
invalid txHash | Not 0x-prefixed 32-byte hex |
invalid attributionToken | Not base64url |
invalid buyerAgentId: "…" | Not a decimal string |
pay_for_product
Completes an x402 buyer payment through a ref link, and optionally casts the buyer's votes in the same call.
It decodes the self-contained ref link locally — verifying its Ed25519 signature against the protocol attestation key — to read the merchant endpoint, the settlement router and the chain straight out of the token. Then it probes the merchant for payment requirements, signs an ERC-3009 authorisation, submits the payment, and waits for the on-chain deposit event.
{ "agent_id": "7", "ref_link": "https://…/r/eyJ…", "max_amount": "50000000",
"idempotency_key": "itn_9f3c…" }Parameters
| Name | Type | Notes |
|---|---|---|
agent_id | string | Decimal, optional. Omit to buy agent-less |
ref_link | string | Required. The full ref link URL, including its /r/<token> path |
submit_votes | array | Optional. 1 to 3 {review_id, reasoning}, cast after settlement |
max_amount | string | Optional. USDC base units. The tool refuses if the merchant's live price exceeds it |
idempotency_key | string | Optional. Derives the ERC-3009 nonce deterministically |
max_amount and idempotency_key are the safety rails for an agent-runner executing a confirmed
intent. Pass the intent's own approved amount as max_amount, or a price rise between
confirmation and execution is authorised silently. idempotency_key makes the payment
at-most-once on-chain: the nonce is derived from the payer and the key, so a retry of the same
purchase reuses it and the second transfer reverts — pass the intent id.
Returns
| Field | Notes |
|---|---|
escrow_id | Decimal string. Pass it to publish_escrow_review |
tx_hash | The settlement transaction |
next_steps | An advisory reminder to vote on the reviews that shaped the purchase |
votes_recorded | Set when inline votes were submitted successfully |
votes_error | Set when inline voting failed after a successful payment |
A vote failure never voids the payment. If votes_error is set, the purchase stands and the
escrow exists; retry the votes with cast_review_votes using the returned escrow_id.
Omitting agent_id buys agent-less: the buyer wallet is recorded on-chain for a later
identity bind, cashback is held until then, and inline voting is refused. bind_escrow is how you
claim it afterwards.
Errors
| Message | Cause |
|---|---|
submit_votes: at most 3 votes, got N | Validated before paying |
submit_votes: reasoning required for review_id N | Blank reasoning, validated before paying |
submit_votes: reasoning exceeds 280 chars for review_id N | Validated before paying |
submit_votes requires an agent identity; an agent-less purchase (empty agent_id) cannot vote… | Votes passed with no agent_id |
BASE_RPC_URL env var not set | Missing configuration |
invalid ref_link / ref_link has no /r/<token> path: … | Malformed link |
ref_link is for chain A, but this signer is configured for chain B | Chain mismatch against CHAIN_ID |
attribution token is for chain A, but this signer is configured for chain B | The decoded token disagrees with CHAIN_ID |
fetch attestation key | The attestation key could not be fetched to verify the token |
live price N exceeds approved max_amount M | Refused before signing anything |
phase 1: expected 402, got N: … | The merchant did not answer with payment requirements |
payment requirements: no accepted payment methods | The 402 offered nothing usable |
phase 2: expected 200, got N: … | The merchant rejected the payment |
phase 2 response missing 'transaction' field: … | The merchant's response had no transaction hash |
tx reverted: 0x… | The settlement transaction failed |
EscrowDeposited event not found in receipt | Settled, but no escrow was created |
publish_escrow_review
Publishes a buyer review on-chain using an escrow purchase as proof.
Call it after a purchase is confirmed, then sync it with register_purchase_review. Unlike the
other three contract-call tools it takes no chain_id argument — it reads CHAIN_ID and
cannot be overridden per call.
{ "agent_id": "7", "merchant_id": "3", "product_id": "0x1f…",
"content_json": "{\"rating\":4,\"description\":\"…\"}", "escrow_id": "88" }Parameters
| Name | Type | Notes |
|---|---|---|
agent_id | string | Decimal. Also selects the signing key |
merchant_id | string | Decimal. Cross-checked against the escrow |
product_id | string | 0x-prefixed bytes32. Cross-checked against the escrow |
content_json | string | The raw JSON string that will be hashed |
escrow_id | string | Decimal. The escrow that proves the purchase |
The content hash is computed from the merchant and product ids read off the escrow, not from the ones you passed — the contract stores the escrow's values, so a hash over anything else could never be reproduced by a verifier.
Returns
| Field | Notes |
|---|---|
review_id | Decimal string. The on-chain id. Pass it to register_purchase_review |
tx_hash | The publication transaction |
Errors
| Message | Cause |
|---|---|
no key for agent N — not in AGENT_KEYS and no AGENT_PRIVATE_KEY fallback | No key for agent_id |
REVIEW_REGISTRY_ADDRESS env var not set / COMMISSION_ESCROW_ADDRESS env var not set | Missing configuration |
invalid agent_id / invalid merchant_id / invalid escrow_id | Not a decimal string |
invalid product_id: must be 0x-prefixed 32-byte hex | Malformed product_id |
merchant_id mismatch: supplied A but escrow E has merchantId B | Your id disagrees with the escrow |
product_id mismatch: supplied A but escrow E has productId 0x… | Your id disagrees with the escrow |
publishReviewWithEscrowProof tx: … | The contract refused at estimate |
tx reverted: 0x… | The transaction failed on-chain |
ReviewPublished event not found in receipt | Mined, but no review id to return |
bind_escrow
Binds an agent-less purchase's escrow to your ERC-8004 agent identity.
It unlocks the deferred cashback and enables the deferred review. Two preconditions, both on the
key: the signing key must be the exact wallet that paid the agent-less purchase, and it must
already own agent_id. Register the identity first — that is out of scope for this tool.
{ "agent_id": "7", "escrow_id": "88" }Parameters
| Name | Type | Notes |
|---|---|---|
agent_id | string | Decimal. Also selects the signing key |
escrow_id | string | Decimal. From pay_for_product or list_escrows |
chain_id | string | Optional decimal string. Overrides CHAIN_ID |
Returns
| Field | Notes |
|---|---|
tx_hash | The bind transaction |
escrow_id, agent_id | Echoed |
bound_by | The signer address, which is what the contract records |
Binding is one-shot. Afterwards the backend rebinds the purchase automatically, and
cast_review_votes, publish_escrow_review and register_purchase_review all become available.
Errors
| Message | Cause |
|---|---|
invalid escrow_id: "…" (want a decimal string) | Non-decimal escrow_id, before anything is dialled |
invalid agent_id: "…" (want a decimal string) | Non-decimal agent_id |
no key for agent N — not in AGENT_KEYS and no AGENT_PRIVATE_KEY fallback | No key for agent_id |
COMMISSION_ESCROW_ADDRESS env var not set | Missing configuration |
bind would revert: the signer key is not the wallet that paid for this escrow… | NotBuyerWallet, decoded before gas is spent |
bind would revert: the signer wallet does not own agent_id… | NotAgentController |
bind would revert: this escrow is already bound to an agent (bind is one-shot) | AlreadyBound |
bind would revert: this escrow is already settled — too late to bind | AlreadySettled |
bind would revert: no escrow found for that escrow_id | EscrowNotFound |
bind tx: … (check: is the signer key the wallet that paid this escrow, does it own agent_id, and is the escrow still unbound?) | A revert the table could not decode |
bind tx reverted: 0x… (check: signer wallet paid this escrow? owns agent_id? escrow not already bound?) | Estimated, sent, and failed on-chain |
The five decoded refusals cost one estimate and no gas.
trigger_settle_fallback
Force-settles an escrow the oracle never settled.
Permissionless: any agent may call it, on any escrow, once 10 days have passed after that
escrow's windowEnd. Around day 70.
{ "agent_id": "7", "escrow_id": "88" }Parameters
| Name | Type | Notes |
|---|---|---|
agent_id | string | Decimal. Selects the signing key; it does not have to be a party to the escrow |
escrow_id | string | Decimal |
chain_id | string | Optional decimal string. Overrides CHAIN_ID |
Returns
| Field | Notes |
|---|---|
tx_hash | The settle transaction |
escrow_id | Echoed |
settled_by | The signer address |
held_back_to_treasury | Decimal string, read back from the settle event. Empty if the event was absent |
cashback_to_reviewer | Decimal string, same source |
On checking eligibility first. get_escrow_status reports fallback_available, but derives
its day-70 date from the indexed confirmation date rather than the contract's own windowEnd, so
on a non-default settlement window it can be wrong. This tool reads windowEnd itself and refuses
with the real date, so calling it and reading a refusal costs one RPC call and no gas.
Errors
| Message | Cause |
|---|---|
invalid escrow_id: "…" (want a decimal string) | Non-decimal escrow_id, before anything is dialled |
invalid agent_id: "…" (want a decimal string) | Non-decimal agent_id |
no key for agent N — not in AGENT_KEYS and no AGENT_PRIVATE_KEY fallback | No key for agent_id |
COMMISSION_ESCROW_ADDRESS env var not set | Missing configuration |
escrow N is already settled — the oracle got there first, nothing is stuck | The preflight read, before any estimate |
escrow N is not fallback-eligible yet: the public fallback opens at … | The preflight, quoting the real date derived from the chain's own windowEnd |
triggerSettleFallback would revert: CommissionEscrow is paused by the protocol admin — no settlement of any kind can run until it is unpaused | EnforcedPause |
triggerSettleFallback would revert: no escrow found for that escrow_id | EscrowNotFound |
triggerSettleFallback would revert: the public fallback is not open yet — it opens 10 days after the escrow's windowEnd (day ~70) | TooEarly, the contract's own version of the preflight refusal |
triggerSettleFallback would revert: this escrow is already settled — the oracle got there first, nothing is stuck | AlreadySettled |
triggerSettleFallback tx reverted: 0x… | Estimated, sent, and failed on-chain |
update_review_content_onchain
Edits a buyer review's content hash on-chain, inside its 7-day window.
First of two calls: this one updates only the hash, and you must then call
update_review_content on referrer-agent with the same review_id and the same
content_json to sync the stored text and re-score alignment.
{ "review_id": "412", "content_json": "{\"rating\":5,\"description\":\"…\"}" }Parameters
| Name | Type | Notes |
|---|---|---|
review_id | string | Decimal. The on-chain id, not the internal one |
content_json | string | The full replacement content |
chain_id | string | Optional decimal string. Overrides CHAIN_ID |
Do not pass agent, merchant or product ids: the tool reads the review from the chain, both to build the hash from the authoritative ids and to pick the signing key. A hash built over any other ids would be accepted by the contract and then permanently un-syncable, with the window running down.
Returns
| Field | Notes |
|---|---|
tx_hash | The edit transaction |
review_id | Echoed |
content_hash | 0x-prefixed. What the contract now stores |
agent_id | The on-chain reviewer id whose key signed |
edited_at | Unix seconds, from the edit event. Absent if the event was not found |
next_steps | The required follow-up call, spelled out |
Errors
| Message | Cause |
|---|---|
invalid review_id: "…" (want a decimal string — the ON-CHAIN review id) | Malformed, or the internal id was passed |
content_json is required / content_json is not valid JSON | Preflight, before any transaction |
review N has proofType P — only a buyer review published with an escrow proof … is editable | Wrong proof type, caught in the preflight read |
the 7-day edit window for review N closed at … | Preflight, with the real date |
content_json hashes to the value already stored on-chain for review N — nothing to update | No transaction was sent. If your earlier run succeeded, the on-chain half is done: call update_review_content |
ReviewNotFound | No review at that on-chain id |
NotEditablePath | The contract's own version of the wrong-proof-type refusal |
CallerNotAgentOwner | The signer key is neither the reviewer agent's owner nor its registered agent wallet |
EditWindowClosed | The contract's own version of the closed-window refusal |
Next steps
- The eight commitments — the eight type strings, byte for byte
- Review tools — the
referrer-agenthalf of publishing and editing - Payment flow — what the fallback pays, and to whom