HORKOSὅρκος · escrow-policy-signer

In Hesiod, Horkos is the child of Strife — the oath, and the punishment of whoever swears falsely.

The signer that refuses

It holds the only key authorized in AIMarketEscrow.authorizedHubs, so the Hub does not. One allowed selector. One escrow. One chain. And for every amount, the authority is the buyer's own EIP-712 signature — re-verified here against state this process read itself.

1allowed selector
292bytes of canonical calldata
26named refusal rules
118tests
1dependency

01What it will sign

Exactly one shape: a canonical debitChannel call, selector 0xf7becd80 derived at boot from its signature string and never written down as a literal, to the one pinned escrow, on chain 8453, with value == 0. Not settleChannel. Not a token transfer. Not setHubAuthorization. Not a contract creation.

The amount is not the Hub's to decide

The digest is rebuilt here from the calldata plus token and nonce read from getChannel, with this service's own address substituted for the hub field — exactly as the contract does. A signature minted for another hub, at another nonce, or by anyone who is not the channel's depositor is refused.

Its own view of chain

Every fact it checks is one the caller would like to supply, so none of them comes from the request: the channel's depositor, token, balance and nonce, whether the receipt was already collected, and whether this key is still an authorized hub. Read over endpoints the caller does not configure. A read that fails is unavailability, never “false”.

02The refusal ladder

Moving the key out of the Hub buys almost nothing on its own: the Hub sends {to, data, chainId, gas, value}, and a naive signer signs whatever it is handed. All the value is in the refusals, so each one has a name, a status code and a test that asserts nothing was signed and nothing was broadcast.

03What it does not fix

residual risk

A compromised Hub can still collect for work it did not do — up to the velocity caps, using signatures buyers really produced. Every field the contract checks sits inside the depositor's digest, so such a request is indistinguishable from a legitimate one. It shrinks with smaller caps, a shorter authorization TTL, and buyers signing per invocation instead of in advance. It cannot be closed inside a signer, and a security page that omitted it would be marketing.

the key is also the payee

settleChannel pays ch.hub, which is this key — so revenue accumulates here rather than in the treasury. Sweeping it is a separate, deliberate operator action.

griefing is free in dollars

Burning a receipt or grinding a nonce costs no USDC at all, so the money caps cannot see it. The count caps are the only bound, which is why they exist separately — and why the fee ceiling is a third, independent number denominated in the key's ETH.

04Plan before you submit

Broadcasting requires four independent things, and no single mistake starts moving money. Plan mode is not a stub: it builds the exact calldata and runs it through eth_call against live contract state, so it answers “would this be accepted right now?” without a transaction existing.

# nothing broadcasts from the request path, in any strategy
docker exec modelmarket-hub python -m aimarket_hub.escrow_bridge.cli plan
docker exec modelmarket-hub python -m aimarket_hub.escrow_bridge.cli submit --yes

# the four gates, in order
AIMARKET_ESCROW_BRIDGE_ENABLED=1
AIMARKET_ESCROW_SUBMIT_STRATEGY=external
AIMARKET_ESCROW_SUBMIT_CONFIRM=i-understand-this-moves-funds
--yes  # the operator is present for THIS run