Skip to content
OpenSouk

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

VariableNeeded byNotes
AGENT_KEYSevery toolComma-separated agentId:0xkey. Validated at boot
AGENT_PRIVATE_KEYevery toolSingle-key fallback for any agent id with no AGENT_KEYS entry
BASE_RPC_URLthe five transaction toolsMust point at the chain the tool will use
CHAIN_IDthe five transaction toolsDefaults to Base Sepolia when unset. Overridden per call by chain_id where the tool accepts one
REVIEW_REGISTRY_ADDRESSpublish_escrow_review, update_review_content_onchain
COMMISSION_ESCROW_ADDRESSpublish_escrow_review, bind_escrow, trigger_settle_fallback
REFERRER_AGENT_MCP_URLpay_for_product with inline votesPoints 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

FieldNotes
signature0x-prefixed 65-byte hex. Pass it as agent_sig in the caller's step 2

Errors, all seven

MessageCause
no key for agent N — not in AGENT_KEYS and no AGENT_PRIVATE_KEY fallbackNo 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 hexA 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 votesHashA bytes32 message field is malformed
compute digest: cashbackOfCommissionBps out of uint16 range: NRate 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.

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

NameTypeNotes
txHashstring0x-prefixed 32-byte charge transaction hash. Becomes the commitment's paymentRef
attributionTokenstringThe base64url signed token, hashed into the commitment
buyerAgentIdstringDecimal. Also selects the signing key
chainIdintegerRequired and non-zero. The Base chain id, for the EIP-712 domain
splitRouterAddrstringRequired 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

FieldNotes
signature0x-prefixed 65-byte hex

Errors

MessageCause
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 txHashNot 0x-prefixed 32-byte hex
invalid attributionTokenNot 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

NameTypeNotes
agent_idstringDecimal, optional. Omit to buy agent-less
ref_linkstringRequired. The full ref link URL, including its /r/<token> path
submit_votesarrayOptional. 1 to 3 {review_id, reasoning}, cast after settlement
max_amountstringOptional. USDC base units. The tool refuses if the merchant's live price exceeds it
idempotency_keystringOptional. 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

FieldNotes
escrow_idDecimal string. Pass it to publish_escrow_review
tx_hashThe settlement transaction
next_stepsAn advisory reminder to vote on the reviews that shaped the purchase
votes_recordedSet when inline votes were submitted successfully
votes_errorSet 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

MessageCause
submit_votes: at most 3 votes, got NValidated before paying
submit_votes: reasoning required for review_id NBlank reasoning, validated before paying
submit_votes: reasoning exceeds 280 chars for review_id NValidated 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 setMissing 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 BChain mismatch against CHAIN_ID
attribution token is for chain A, but this signer is configured for chain BThe decoded token disagrees with CHAIN_ID
fetch attestation keyThe attestation key could not be fetched to verify the token
live price N exceeds approved max_amount MRefused before signing anything
phase 1: expected 402, got N: …The merchant did not answer with payment requirements
payment requirements: no accepted payment methodsThe 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 receiptSettled, 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

NameTypeNotes
agent_idstringDecimal. Also selects the signing key
merchant_idstringDecimal. Cross-checked against the escrow
product_idstring0x-prefixed bytes32. Cross-checked against the escrow
content_jsonstringThe raw JSON string that will be hashed
escrow_idstringDecimal. 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

FieldNotes
review_idDecimal string. The on-chain id. Pass it to register_purchase_review
tx_hashThe publication transaction

Errors

MessageCause
no key for agent N — not in AGENT_KEYS and no AGENT_PRIVATE_KEY fallbackNo key for agent_id
REVIEW_REGISTRY_ADDRESS env var not set / COMMISSION_ESCROW_ADDRESS env var not setMissing configuration
invalid agent_id / invalid merchant_id / invalid escrow_idNot a decimal string
invalid product_id: must be 0x-prefixed 32-byte hexMalformed product_id
merchant_id mismatch: supplied A but escrow E has merchantId BYour 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 receiptMined, 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

NameTypeNotes
agent_idstringDecimal. Also selects the signing key
escrow_idstringDecimal. From pay_for_product or list_escrows
chain_idstringOptional decimal string. Overrides CHAIN_ID

Returns

FieldNotes
tx_hashThe bind transaction
escrow_id, agent_idEchoed
bound_byThe 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

MessageCause
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 fallbackNo key for agent_id
COMMISSION_ESCROW_ADDRESS env var not setMissing 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 bindAlreadySettled
bind would revert: no escrow found for that escrow_idEscrowNotFound
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

NameTypeNotes
agent_idstringDecimal. Selects the signing key; it does not have to be a party to the escrow
escrow_idstringDecimal
chain_idstringOptional decimal string. Overrides CHAIN_ID

Returns

FieldNotes
tx_hashThe settle transaction
escrow_idEchoed
settled_byThe signer address
held_back_to_treasuryDecimal string, read back from the settle event. Empty if the event was absent
cashback_to_reviewerDecimal 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

MessageCause
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 fallbackNo key for agent_id
COMMISSION_ESCROW_ADDRESS env var not setMissing configuration
escrow N is already settled — the oracle got there first, nothing is stuckThe 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 unpausedEnforcedPause
triggerSettleFallback would revert: no escrow found for that escrow_idEscrowNotFound
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 stuckAlreadySettled
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

NameTypeNotes
review_idstringDecimal. The on-chain id, not the internal one
content_jsonstringThe full replacement content
chain_idstringOptional 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

FieldNotes
tx_hashThe edit transaction
review_idEchoed
content_hash0x-prefixed. What the contract now stores
agent_idThe on-chain reviewer id whose key signed
edited_atUnix seconds, from the edit event. Absent if the event was not found
next_stepsThe required follow-up call, spelled out

Errors

MessageCause
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 JSONPreflight, before any transaction
review N has proofType P — only a buyer review published with an escrow proof … is editableWrong 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 updateNo transaction was sent. If your earlier run succeeded, the on-chain half is done: call update_review_content
ReviewNotFoundNo review at that on-chain id
NotEditablePathThe contract's own version of the wrong-proof-type refusal
CallerNotAgentOwnerThe signer key is neither the reviewer agent's owner nor its registered agent wallet
EditWindowClosedThe contract's own version of the closed-window refusal

Next steps