seal. Docs Try the sandbox ↗
Mainnet is not yet live. Explore with demo funds or test tokens. Review status ↗

SEAL ZK Intent Service

The service provides execution of intents authorized by users. An intent includes the operation, its authorized execution terms, an accepted fee quote, and the proof or signature required for that operation. The executor signs the resulting transaction and pays gas; contracts enforce the authorized terms.

Use the standalone intent service guide: make init creates an executor and private .env; make run prints its address and balance and starts the HTTPS service. Supply one database URL. Only valid declined intents are posted to the deferred-intent PostgreSQL tables; execution recovery and earnings remain local. Encrypted transfer delivery uses a separate durable schema.

Hosted Sepolian intent service

The selected deployment configuration, or the build-time VITE_SEAL_RELAYER binding for a testnet run, supplies an HTTPS relayerUrl, such as https://intent.sealpool.app, with private-queue-v1. The frontend discovers the submission key from that trusted origin and sends encrypted intents; it receives no database credentials. The browser-only sandbox does not submit transactions. The submission selector checks /relay/info and shows Reachable or Offline. Reachable means the API is reachable for the selected chain and pool; execution still requires a funded executor and a valid quote.

Select an installed fresh mock run with SEAL_TESTNET_RUN, run make init once and make run to start the service. Initialization selects simulate; review the configuration before enabling capped test-subsidy signing and funding the printed executor with Sepolia ETH. Mock-token fees do not cover real gas costs. Back up .env and the local encrypted journal before funding. See the single-service guide.

Intent execution and transfer delivery are separate. A deployment using recoveryMode: "local" with transferDelivery: "relayer" tries encrypted delivery through the HTTPS service and falls back to sharing a file when that service is unavailable. Wallet-paid transfers use the same optional delivery path. See Private transfers and Transfer delivery.

The frontend has no direct PostgreSQL path and never receives database credentials. An installed Sepolia worker keeps its database and executor when make run selects a verified deployment; recovery preserves nonce and spending state.

Mock testnet

The mock testnet uses the completed deployment in config/sepolia, or an explicitly selected SEAL_TESTNET_RUN. First-time installations initialize in simulate mode; installed workers keep their private environment and restart with make run. SQL migration numbers describe installation order and are independent of the v1 wire format.

Swap quotes bind the configured canonical adapter. Strict action decoding and exact-call simulation check the proof-bound action; swaps pay their execution fee in the input asset and require no private fee-note payload. Direct and queued workers carry the validated destination into simulation, signing and restart validation. Pin the pool, fixed verifier bundle, fee controller, swap adapter and factory; an arbitrary client target is never accepted.

Leverage uses the configured marginAdapter and a separate actionGas.margin envelope. Pin its adapter, immutable account implementation, DEX factory and fee contract. Verify that the implementation binds the same adapter, factory and Aave market. The current testnet release has 19 distinct pins with swaps and both margin extensions installed; the intent service verifies every supplied pin, including the public margin contracts, and rejects missing required dependencies. Every private action, including the first OPEN of a new account, carries the position key's 65-byte EIP-712 AccountMarginAction signature (domain SEAL Margin, chain ID and adapter). It binds the action hash and checkpoint-update hash. Funded actions (OPEN, ADD_COLLATERAL, REPAY) use a 71-signal SEAL withdrawal proof containing the checkpoint transition; execution.stateProof is empty. Unfunded actions use an 11-signal execution.stateProof and an empty monetary proof. The proof context is the same signed digest. The signed minHealthFactor floor (1e18 = 1.0; zero disables) is part of the digest and calldata. Every margin request requires an authentic, unexpired quote and its exact request reference. Reservations include the account nonce so competing portfolio actions cannot be signed concurrently. The intent service never receives the controller private key. testnet:deploy, relayer:initialize and make init carry the selected margin targets into their generated configurations. /relay/info advertises swapAdapter, marginAdapter, publicMarginAdapter and enabled operations.

All Rust HTTP intent quotes, including public and private margin, default to 1 hour. POST /relay/quote accepts optional numeric timeoutSeconds (0–31536000); the selected deadline is covered by the quote MAC and bound into the execution action. The UI exposes minutes under Advanced and requests fresh quotes after a change. 0 disables practical execution expiry with the contract-compatible deadline 9007199254740991; SEAL proofs use the circuit's actual expiration = 0 sentinel. Contracts reject deadline zero, so it must never be sent as an execution deadline. Swap route quotes can impose an earlier market-validity deadline. Signed health, slippage and fee limits still apply. A small execution fee to cover gas applies. Review the current quote before authorizing the intent. The fee asset is the debt asset for REPAY and the collateral asset otherwise. The adapters cap the execution fee at 10% of funded margin (OPEN, ADD_COLLATERAL, REPAY) or of released proceeds (REDUCE, CLOSE). A quote request may add "margin": "<funded amount in fee-asset base units>"; the intent service then refuses a fee above 10% of it with HTTP 422 and the minimum funded size. Without it, admission rejects an undersized funded action at submission. Margin quotes echo "batch", the number of actions they price (1 unless the request asked for a batch).

Public margin relay (publicMargin)

Wallet-funded positions use SealPublicMarginAdapter. Configure it with the Rust config field publicMarginAdapter plus actionGas.publicMargin (both or neither; generators use 2,500,000 gas). Startup verifies that the adapter's POOL() is the configured pool; the Rust validator also pins the adapter and its account implementation, factory and fee contract. Quotes for this operation carry publicMarginAdapter. The submission is:

{
  "quoteId": "0x…",
  "proof": {
    "data": "0x",
    "verifierVersion": "0",
    "signals": ["0", "…71 zeros in total"]
  },
  "execution": {
    "action": { "…": "MarginAction" },
    "signature": "0x…",
    "permitSignature": "0x"
  }
}

It has no feePayload. signature is the controller's non-empty EIP-712 signature (domain SEAL Public Margin): 65 bytes for an EOA, at most 2,048 bytes for an ERC-1271 smart account. permitSignature is "0x" or a 65-byte EIP-2612 permit for margin + maxProtocolFee + relayerFee with the action deadline. Admission requires relayer = executor and relayerFee, deadline and fee asset equal to the quote; operation ≤ CLOSE (4); pendingShield false; and zero payoutRecipient, precommitments, minSharesOut, minRefundShares and recoveryHash. Funded operations need margin > 0 and relayerFee ≤ margin / 10; REDUCE and CLOSE need margin = 0 and no permit. The adapter pulls funding from the controller, never from the intent service, and pays the fee to relayer only on success. The account nonce is reserved as for private margin, and the exact execute(action, signature, permitSignature) call is simulated before signing.

Margin batches (executeBatch)

Both adapters run 1 to 8 REDUCE or CLOSE actions on one account atomically. Public execution uses executeBatch(actions, signature) and the EIP-712 MarginBatch. Private execution uses executeBatch(actions, stateProof, signature) and AccountMarginBatch, binding the ordered action hash and checkpoint-update hash. Closing every market of a cross-margin account is one transaction. The intent service carries batches under the existing operation names, margin for private accounts and publicMargin for public ones.

A batch quote request adds an integer batch (1 to 8; default 1):

{ "operation": "publicMargin", "feeAsset": "0x…", "batch": 3 }

Other operations reject batch. The network gas envelope is actionGas[operation] × batch. The execution fee is quoted for the requested batch. Margin quotes echo "batch": n; the Rust intent service binds it into the quote MAC. Quote requests accept timeoutSeconds from 0 to 31,536,000; the default is 1,800 seconds and 0 selects the authenticated no-timeout deadline. feeAsset is the collateral of the last action, which carries the fee. The funded margin minimum does not apply to a batch, because REDUCE and CLOSE are unfunded.

The public submission is:

{
  "quoteId": "0x…",
  "proof": {
    "data": "0x",
    "verifierVersion": "0",
    "signals": ["0", "…71 zeros in total"]
  },
  "execution": {
    "actions": [{ "…": "MarginAction" }, { "…": "MarginAction" }],
    "signature": "0x…"
  }
}

An execution carries action (one action) or actions (a batch), never both. A batch has no permitSignature and no feePayload. Admission requires:

The Rust intent service reserves every step's account nonce with the single-action key keccak256("seal.margin.reservation.v1", chainId, adapter, account, nonce). A concurrent single action or batch on any of those nonces therefore conflicts. Private submissions additionally include execution.stateProof with 11 signals and a 768-byte proof. Its owner binding and update hash must match the signature. The Rust runtime simulates the exact adapter call, including this proof, before signing. The Rust intent service simulates under a gas cap of the largest action envelope × batch, requires the signed gas limit to fit actionGas × batch and requires the calldata to fit the priced transaction size (unsignedBytes). The Rust intent service's encrypted intent is bounded at 16 KiB of plaintext, about 1 KB per step.

Submission failures and account recovery

HTTP 202 acknowledges a durable intent, not a confirmed transaction. Broadcast or deferred-database failures after local journal persistence return the saved state; exact retries reuse the original ciphertext, signed bytes and nonce. Simulation mode finishes as terminal simulation_complete rather than reserving notes indefinitely. Status includes executionState and an optional reason for deferred policy decisions and retry failures. Saved intents that fail simulation during a retry become terminal rejections; temporary RPC or deployment failures remain queued with a safe error code.

Submission errors return JSON error codes: invalid_intent, simulation_failed (with an allowlisted contract-error reason), submission_conflict, rpc_unavailable, deployment_unavailable, recovery_storage_unavailable, service_at_capacity, or admission_unavailable. HTTP 504 returns execution_response_timeout; work may still finish, so reconcile the saved request. Provider text, credentials, transaction data and private openings are never returned.

The UI archives terminal rejected jobs and unlocks their input notes. Refresh checks canonical inclusion and competing spends before releasing expired jobs, even if the service is offline. Lost responses and temporary outages retain the exact saved submission for status/retry; prepared account recovery data does not imply that a note was spent or an output minted.