Xochi delegation envelope: an EIP-712-signed authorization from a Xochi Member to a specific agent wallet, scoped to particular endpoints with a budget.
Per the Xochi design (locked 2026-04-27 in
xochi/docs/planning/agent-auth.md), a Mandate is a per-request
envelope the agent presents on every protected Xochi call via
X-Xochi-Delegation. Xochi's worker verifies the signature and
decrements budget counters in KV keyed by H(envelope). The agent
never authenticates; it just holds and presents the envelope.
This module's job is to issue, hold, and present envelopes from the raxol side. It does not enforce budgets locally -- that's Xochi's responsibility.
Wire format
X-Xochi-Delegation: base64url(JSON({
message: <MandateMessage>,
signature: "0x" <> <130 hex chars>
}))Schema
Matches xochi/packages/shared/src/eip712.ts:182-263 exactly --
the type hash and field ordering must agree, or signatures won't
verify on the Xochi side.
Mandate(
address human_wallet,
address agent_wallet,
string[] scopes,
uint256 max_amount_usd,
uint256 max_calls,
uint256 expires_at,
bytes32 nonce
)
EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)
name = "Xochi Mandate"
version = "1"
chainId = 1
verifyingContract = 0x0000000000000000000000000000000000000000Chain ID is pinned to mainnet by convention. The envelope is metadata, not a transaction; the pin keeps the type hash stable across deployments.
Summary
Functions
List the scopes Xochi recognizes.
Build an unsigned Mandate, validating fields against the Xochi Zod
constraints in xochi/packages/shared/src/schemas.ts:197-211.
Compute H(envelope) = keccak256(canonical_envelope_json). Matches
the key Xochi uses in KV for per-envelope budget counters.
Return true when the Mandate's scopes include the given scope.
Compute the EIP-712 digest of a Mandate message via
Raxol.Payments.EIP712.hash/3. The string[] scopes field is
encoded per EIP-712 array rules
(keccak256(concat(keccak256(s_i)))) by the shared encoder.
Return true when the Mandate has passed its expires_at.
Decode a base64url envelope back into a Mandate struct. Does not
verify the signature; call verify/1 separately if needed.
Sign a Mandate with the given wallet module.
Encode a signed Mandate as a base64url envelope suitable for the
X-Xochi-Delegation header.
Return the EIP-712 typed-data tuple {domain, types, message} for a
Mandate. Matches viem's buildMandateEip712Data output.
Verify a signed Mandate.
Types
@type hex_address() :: String.t()
@type hex_bytes32() :: String.t()
@type hex_signature() :: String.t()
@type scope() :: String.t()
@type t() :: %Raxol.Payments.Mandate{ agent_wallet: hex_address(), created_at: integer(), envelope_hash: <<_::256>> | nil, expires_at: pos_integer(), human_wallet: hex_address(), max_amount_usd: non_neg_integer(), max_calls: pos_integer(), nonce: hex_bytes32(), scopes: [scope()], signature: hex_signature() | nil }
Functions
@spec allowed_scopes() :: [scope()]
List the scopes Xochi recognizes.
Build an unsigned Mandate, validating fields against the Xochi Zod
constraints in xochi/packages/shared/src/schemas.ts:197-211.
Generates a random 32-byte nonce when omitted.
@spec compute_envelope_hash(t()) :: <<_::256>>
Compute H(envelope) = keccak256(canonical_envelope_json). Matches
the key Xochi uses in KV for per-envelope budget counters.
Return true when the Mandate's scopes include the given scope.
Compute the EIP-712 digest of a Mandate message via
Raxol.Payments.EIP712.hash/3. The string[] scopes field is
encoded per EIP-712 array rules
(keccak256(concat(keccak256(s_i)))) by the shared encoder.
Return true when the Mandate has passed its expires_at.
Decode a base64url envelope back into a Mandate struct. Does not
verify the signature; call verify/1 separately if needed.
Sign a Mandate with the given wallet module.
The wallet must implement Raxol.Payments.Wallet. Sets
:signature and :envelope_hash on the returned struct. The
:signature is the 0x-prefixed 65-byte hex form expected by Xochi's
Zod schema; :envelope_hash is keccak256(envelope_json), the same
key Xochi uses in KV for budget counters.
Encode a signed Mandate as a base64url envelope suitable for the
X-Xochi-Delegation header.
Return the EIP-712 typed-data tuple {domain, types, message} for a
Mandate. Matches viem's buildMandateEip712Data output.
Intended for inspection or external signing; sign/2 does not call
this -- it computes the digest directly because the string[] field
needs array-aware encoding that Raxol.Payments.EIP712 does not
provide.
@spec verify(t()) :: :ok | {:error, :unsigned | :invalid_signature | :unauthorized_signer}
Verify a signed Mandate.
Recovers the signer's address from the signature and compares to
:human_wallet. Returns :ok on match or {:error, reason}. A
mutated field changes the digest, which changes the recovered
signer -- so tamper detection falls out of this check.