Il mandato
Il mandato è il documento con cui il titolare autorizza un agente a spendere per conto suo. Lo firma il titolare, lo presenta l'agente a ogni finanziamento, e lo fa valere il MandateHook sulla chain. Se il job non ci sta dentro, il finanziamento non avviene e il motivo arriva leggibile.
I campi
| Campo | Tipo | Cosa vuol dire |
|---|---|---|
owner |
address |
Il titolare. Firma il mandato e ne risponde. |
agent |
address |
La chiave dell'agente, che deve essere il cliente del job. Mai uguale al titolare. |
maxPerJob |
uint256 |
Il massimo per un singolo job, in unità minime di EURC (6 decimali). |
capPerPeriod |
uint256 |
Il tetto di spesa per periodo. |
period |
uint64 |
La durata del periodo in secondi. Zero vuol dire un periodo solo per tutta la vita del mandato. |
payeesRoot |
bytes32 |
La radice di Merkle dei fornitori ammessi. Zero vuol dire chiunque. |
validUntil |
uint64 |
Dopo questo istante il mandato non vale più. |
allowSelfEvaluation |
bool |
Se vero, l'agente può essere l'evaluator dei suoi job. |
nonce |
bytes32 |
Casuale: due mandati con gli stessi termini restano distinti. |
La firma
Il mandato si firma in EIP-712, nel dominio del MandateHook:
{
"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"
}
L'hash EIP-712 è l'id del mandato: lo stesso nell'API, nel registro e sulla chain. Una firma vale per un solo MandateHook su una sola chain.
Il titolare può essere un wallet oppure uno smart account: il hook accetta le firme ERC-1271, quindi un'azienda può firmare dal suo multisig.
Firmarlo e registrarlo
Per il pilota c'è una CLI, che firma con una chiave in una variabile d'ambiente e registra il mandato:
OWNER_PRIVATE_KEY=0x... SCONTRACT_API_URL=https://scontratto-api.scontratto.workers.dev \
node apps/scontract-mcp/dist/mandato.mjs \
--agent 0xAGENTE \
--per-job 50 --per-period 200 --period-days 30 \
--valid-days 90 \
--provider 0xFORNITORE_1 --provider 0xFORNITORE_2
| Opzione | Predefinito | Cosa fa |
|---|---|---|
--agent, --agente |
obbligatoria | L'indirizzo dell'agente. |
--per-job |
obbligatoria | Il massimo per job, in EURC. |
--per-period, --per-periodo |
obbligatoria | Il tetto per periodo, in EURC. Non può essere minore del massimo per job. |
--period-days, --giorni-periodo |
30 |
La durata del periodo. Con 0, un periodo solo. |
--valid-days, --valido-giorni |
90 |
La durata del mandato. |
--provider, --fornitore |
nessuno | Un fornitore ammesso. Si ripete. Senza, vale chiunque. |
--self-evaluation, --auto-valutazione |
spento | L'agente può essere l'evaluator dei suoi job. |
I messaggi della CLI seguono SCONTRACT_LANG, come il server MCP.
Chi firma dal proprio wallet può costruire il mandato da sé e registrarlo con POST /v1/mandates: vedi API.
I fornitori ammessi
La lista in chiaro resta nel registro. Sulla chain va solo la sua radice di Merkle. Ogni foglia è keccak256 dei 20 byte dell'indirizzo, e ogni coppia si hasha in ordine crescente, come fa MerkleProof di OpenZeppelin. Un nodo senza fratello sale da solo. Con un solo fornitore, la radice è la sua foglia e la prova è vuota.
L'agente allega la prova per il fornitore del job a ogni finanziamento. Il server MCP la calcola da solo dalla lista registrata. L'ordine della lista conta, perché fa parte del documento firmato.
I periodi
Il periodo è una finestra fissa: timestamp / period. Tutto quello che l'agente finanzia in una finestra si somma, e non può superare capPerPeriod. Alla finestra successiva il conto riparte da zero.
Un job respinto dall'evaluator libera subito la sua parte di tetto. Un job scaduto la libera quando qualcuno chiama MandateHook.release(jobId): il cron di scontract.ai lo fa insieme al rimborso. Un job completato resta speso.
La revoca
Il titolare revoca chiamando MandateHook.revoke(mandato) dal suo wallet, o dal suo smart account. Serve un po' di ETH per il gas. Da quel momento nessun job nuovo si può finanziare sotto quel mandato. I job già finanziati seguono il loro corso.
Cosa controlla il hook
Prima di ogni fund, nell'ordine:
- Il cliente del job è l'agente del mandato.
- La firma è del titolare.
- Il mandato non è revocato.
- Il mandato non è scaduto.
- Il prezzo non supera il massimo per job.
- Con questo prezzo, il periodo non supera il tetto.
- Il fornitore è ammesso.
- L'evaluator non è il fornitore, e non è l'agente se il mandato non lo consente.
L'API fa gli stessi controlli prima di mandare la transazione, così un rifiuto costa zero gas e arriva con il suo motivo.
Dopo la Fase 0
Oggi il titolare è un wallet. Nella v2 il mandato diventa la credenziale derivata di Agent Rail: firmata in QES e radicata nella CIE o nell'EUDI Wallet del titolare. Nella v3 arrivano le persone giuridiche, con i poteri di firma dal Registro Imprese. Il contratto resta lo stesso: cambia chi può firmare.