ITEN
Demo · Base Sepolia · test money

The mandate

The mandate is the document with which the owner authorizes an agent to spend on their behalf. The owner signs it, the agent presents it at every funding, and the MandateHook enforces it on the chain. If the job does not fit within it, the funding does not happen and the reason comes back in readable form.

The fields

Field Type What it means
owner address The owner. Signs the mandate and answers for it.
agent address The agent's key, which must be the client of the job. Never the same as the owner.
maxPerJob uint256 The maximum for a single job, in EURC base units (6 decimals).
capPerPeriod uint256 The spending cap per period.
period uint64 The length of the period in seconds. Zero means a single period for the whole life of the mandate.
payeesRoot bytes32 The Merkle root of the allowed providers. Zero means anyone.
validUntil uint64 After this moment the mandate is no longer valid.
allowSelfEvaluation bool If true, the agent can be the evaluator of its own jobs.
nonce bytes32 Random: two mandates with the same terms stay distinct.

The signature

The mandate is signed with EIP-712, in the MandateHook domain:

{
  "domain": { "name": "scontract.ai", "version": "1", "chainId": 84532, "verifyingContract": "<MandateHook>" },
  "types": {
    "Mandate": [
      { "name": "owner", "type": "address" },
      { "name": "agent", "type": "address" },
      { "name": "maxPerJob", "type": "uint256" },
      { "name": "capPerPeriod", "type": "uint256" },
      { "name": "period", "type": "uint64" },
      { "name": "payeesRoot", "type": "bytes32" },
      { "name": "validUntil", "type": "uint64" },
      { "name": "allowSelfEvaluation", "type": "bool" },
      { "name": "nonce", "type": "bytes32" }
    ]
  },
  "primaryType": "Mandate"
}

The EIP-712 hash is the mandate id: the same in the API, in the registry and on the chain. A signature is valid for only one MandateHook on only one chain.

The owner can be a wallet or a smart account: the hook accepts ERC-1271 signatures, so a company can sign from its multisig.

Signing and registering it

For the pilot there is a CLI that signs with a key in an environment variable and registers the mandate:

OWNER_PRIVATE_KEY=0x... SCONTRACT_API_URL=https://scontratto-api.scontratto.workers.dev \
  node apps/scontract-mcp/dist/mandato.mjs \
  --agent 0xAGENT \
  --per-job 50 --per-period 200 --period-days 30 \
  --valid-days 90 \
  --provider 0xPROVIDER_1 --provider 0xPROVIDER_2
Option Default What it does
--agent required The agent's address.
--per-job required The per-job maximum, in EURC.
--per-period required The per-period cap, in EURC. It cannot be lower than the per-job maximum.
--period-days 30 The length of the period, in days. With 0, a single period.
--valid-days 90 How long the mandate lasts, in days.
--provider none An allowed provider. Repeatable. Without it, anyone is allowed.
--self-evaluation off The agent can be the evaluator of its own jobs.

The Italian flags (--agente, --per-periodo, --giorni-periodo, --valido-giorni, --fornitore, --auto-valutazione) still work. Set SCONTRACT_LANG=en for English messages.

If you sign from your own wallet, you can build the mandate yourself and register it with POST /v1/mandates: see API.

Allowed providers

The plain-text list stays in the registry. Only its Merkle root goes on the chain. Each leaf is the keccak256 of the 20 bytes of the address, and each pair is hashed in ascending order, as OpenZeppelin's MerkleProof does. A node without a sibling moves up on its own. With a single provider, the root is its leaf and the proof is empty.

The agent attaches the proof for the job's provider to every funding. The MCP server computes it on its own from the registered list. The order of the list matters, because it is part of the signed document.

Periods

The period is a fixed window: timestamp / period. Everything the agent funds in one window adds up, and cannot exceed capPerPeriod. In the next window the count starts again from zero.

A job rejected by the evaluator frees its share of the cap immediately. An expired job frees it when someone calls MandateHook.release(jobId): the scontract.ai cron does this together with the refund. A completed job stays spent.

Revocation

The owner revokes by calling MandateHook.revoke(mandate) from their wallet, or from their smart account. This needs a little ETH for gas. From that moment no new job can be funded under that mandate. Jobs already funded follow their course.

What the hook checks

Before every fund, in order:

  1. The job's client is the mandate's agent.
  2. The signature is the owner's.
  3. The mandate is not revoked.
  4. The mandate has not expired.
  5. The price does not exceed the per-job maximum.
  6. With this price, the period does not exceed the cap.
  7. The provider is allowed.
  8. The evaluator is not the provider, and is not the agent unless the mandate allows it.

The API runs the same checks before sending the transaction, so a refusal costs zero gas and comes with its reason.

After Phase 0

Today the owner is a wallet. In v2 the mandate becomes the derived credential of Agent Rail: signed with a QES and rooted in the owner's CIE or EUDI Wallet. In v3 legal entities arrive, with signing powers from the Registro Imprese. The contract stays the same: what changes is who can sign.