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

Run the ZK Intent Service

For a fresh mock testnet, first install/verify the selected run as described in the testnet runbook. From the repository root:

SEAL_TESTNET_RUN=/absolute/path/to/selected-run make init
make run

make init creates a new Ethereum executor wallet and saves its private keys, one SEAL_DATABASE_URL, RPC URL and local storage paths in a private .env. Set a fresh isolated database URL explicitly. It refuses to replace an existing .env; restarting never creates a new wallet.

The recommended HTTPS origin is https://intent.sealpool.app; configure DNS, TLS and the reverse proxy before pointing clients at it. Initialization does not require an existing intent service URL. Set SEAL_RELAYER_ORIGIN to the intended HTTPS origin during initialization or in the private .env before publishing the endpoint.

The Makefile selects stable Rust through rustup and installs that toolchain if needed. It also finds rustup under ~/.cargo/bin when your shell has not added that directory to PATH. If rustup itself is missing, install it from https://rustup.rs and retry make init. Run rustup update stable to update an existing stable installation.

make run builds and starts the Rust intent service directly. Initialization, HTTP, PostgreSQL, pricing, signing and recovery all run in Rust. Node, npm, pg, psql and an external flock command are not runtime dependencies. It prints the executor address and ETH balance and serves behind your HTTPS proxy. Fund the address it prints with Sepolia test ETH. Funding an empty wallet does not require a restart. Leave the terminal running; Ctrl+C stops the service. Run make run again to resume. Only one process may run a given executor on this host.

The existing nginx certificate and renewal configuration are preserved. On a new host, install Rust 1.94.1+ and OpenSSL development libraries, then configure nginx/Certbot to proxy your HTTPS domain to 127.0.0.1:8789. The Rust service only binds to loopback and does not change nginx or certificates.

To restart an instance for the same deployment, stop its process, pull the repository and run make run. Keep the existing .env and .local/testnet-v1/relayer/ directory; do not run make init again. The Rust service reads the existing encrypted AES-GCM journal, including signed transactions, nonce reservations and earned fee notes. The wallet lock also uses the existing executor-specific lock path.

Swap and margin actions

The service accepts these operations when they are enabled in its configuration:

Operation Transaction target Initial gas estimate
settle, transfer, withdraw SEAL pool 1,500,000
deposit Configured depositAdapter 1,500,000
swap Configured swapAdapter 3,000,000
margin Configured private marginAdapter 3,500,000
publicMargin Configured public publicMarginAdapter 2,500,000

make init reads the adapter addresses from the selected deployment (margin.publicAdapter for public margin) and adds their gas estimates to actionGas. An absent adapter leaves its operation disabled. The estimates bound admission and pricing; they are not measured costs. /relay/info exposes the enabled operations and every configured adapter address.

For an existing instance, preserve its keys and journal. Before enabling an additional action, reconcile outstanding transactions and update the file named by SEAL_RELAYER_CONFIG with the adapter address, actionGas entry and reviewed code hashes from the same deployed run. Restart make run and check /relay/info. Pulling new code alone does not enable an action in an existing configuration. On Sepolia, make run selects the checked-in deployment with the existing .env and executor. It verifies contracts, reconciles saved signed transactions, archives the encrypted journal and preserves nonce/spending state before updating the saved deployment bindings. See startup recovery intent-service-rs/SETUP.md.

Startup verifies every configured runtime code hash and requires the dependencies discovered from the pool and enabled adapters. Private margin verification covers its account implementation, DEX factory and fee contract. The current release pins 19 contracts when deposits, both margin extensions and swaps are installed, including all four proof routes. Missing or changed pins prevent startup.

Private swaps and margin actions use authenticated quotes bound to the adapter. Every margin action, including a first OPEN, carries its controller's EIP-712 signature and account nonce. Private actions require a checkpoint transition; funded actions include it in the withdrawal proof and unfunded actions use an account-only proof. Public margin quotes expire within 180 seconds and private margin quotes within 600 seconds, because a funded private action binds the deadline into a browser proof. Closing a private margin position settles its remaining proceeds into SEAL. Relayed public margin actions pull funding from the signing wallet (allowance or EIP-2612 permit) and return proceeds to it; see public margin relay. The intent service receives neither the controller's private key nor the returned note's spending key. Deposits support direct wallet submission or an EIP-712 deposit intent with a checkpoint proof. An EIP-2612 permit supplies token authorization when supported; the service quote and fee use the deposited asset.

Logs and fee quotes

The foreground CLI writes one JSON log per event: intent received, transaction signed, transaction broadcast, transaction confirmed, intent deferred, intent rejected, transfer data received, and retry delays. Transaction events include the public transaction hash once one exists. wallet balance reports the executor's pending ETH balance every minute and marks an empty wallet as needing funding. Logs omit encrypted request bodies, private keys, note openings, database URLs and raw provider errors.

A small execution fee to cover gas applies. Review the current quote before submitting your intent for execution. Request a quote with the desired feeAsset from /relay/quote; the response supplies the exact fee, payment asset, executor and deadline to authorize. Changing these terms requires a new proof or signature.

The service simulates the finished transaction before signing and before each broadcast attempt. Missing or stale required prices, failed simulations, insufficient fee coverage, funding or execution budgets prevent execution. The quoted fee is fixed for the authorized intent; actual gas expenditure can differ and is not reconciled after execution. Sepolia test-token reference prices do not establish real profitability. Quote failures log safe stage labels without provider URLs or credentials.

What happens to an intent

  1. The frontend requests a fee quote over HTTPS and encrypts its intent.
  2. The service authenticates the quote, validates the proof or controller signature, verifies the deployed contracts, and simulates the exact transaction onchain.
  3. If the fee covers current costs and execution meets the available balance and gas budgets, the service signs it, saves exact recovery bytes locally, and broadcasts immediately. It makes no PostgreSQL call on this path.
  4. Valid intents declined because of price, budget, simulation-only policy or insufficient ETH are saved encrypted in seal_deferred.intents using the single supplied database URL. Invalid proofs and expired new submissions are rejected without insertion.
  5. Deferred intents are rechecked. Before executing one later, the service removes it from PostgreSQL. If deletion cannot be confirmed, it does not broadcast. Expired deferred intents are removed.

Acceptance is not confirmation of execution. An accepted intent may execute before its deadline even after the browser closes. The standalone status response includes executionState (deferred, signed, broadcast, confirmed or rejected) and the queue state field. Only confirmed indicates successful confirmed execution; clients still reconcile against the chain. Invalid or expired new intents return HTTP 422, conflicting submissions return 409, and temporary failures return 503 with Retry-After. An uncertain response does not cancel an intent: check status and preserve the original saved request.

PostgreSQL stores deferred intents and, in a separate seal_delivery schema, recipient-encrypted transfer envelopes. It is not the signing journal. No additional database logins or password resets are required. A database outage does not prevent immediate execution; it does prevent storing or promoting deferred intents reliably. Transfer delivery falls back to an encrypted file. Deleting or expiring an intent never deletes its transfer envelopes.

The local encrypted journal under .local/testnet-v1/relayer/ holds nonce and budget reservations, pending signed bytes, short-lived status records and the intent service's own fee-note openings. An uncertain broadcast is recovered by replaying its exact bytes. Confirmed/rejected status records expire after a day; earned fee notes remain local. Back up .env and this directory together. Do not copy the same executor to another active host or delete its recovery journal to clear pending transactions.

Enable encrypted transfer delivery

After reviewing the additive migration intent-service-rs/migrations/004_transfer_delivery.sql, run once on the intent service host, then restart make run:

make delivery-migrate
make run
curl --fail https://intent.sealpool.app/archive/health

The migration reads the existing private .env and uses SEAL_DATABASE_URL. It creates only the seal_delivery schema and leaves existing queues, logins, passwords and account backups unchanged. Requests and ordinary restarts never run this delivery migration. A separate server-only SEAL_TRANSFER_DATABASE_URL is optional. Do not put either URL in frontend variables. The browser uses HTTPS /archive/* endpoints. Add this route to the existing HTTPS server block (preserve certificates and the other routes), then run sudo nginx -t && sudo systemctl reload nginx:

location /archive/ {
    client_max_body_size 2m;
    include /etc/nginx/snippets/seal-relayer-proxy.conf;
}

The shared proxy snippet forwards to 127.0.0.1:8789 without logging or disk request buffering. The current deployment template includes this route; the 2 MiB override is needed for encrypted batches because the server-wide limit is 64 KiB. Find the active HTTPS server block with sudo nginx -T, keep that output private, and back up the configuration outside nginx's included directories before editing. Preserve the certificates, other routes and default-deny location. If the host has no proxy snippet, use these directives inside the location instead:

proxy_pass http://127.0.0.1:8789;
proxy_http_version 1.1;
proxy_request_buffering off;
proxy_buffering off;
proxy_max_temp_file_size 0;
proxy_connect_timeout 5s;
proxy_read_timeout 40s;
proxy_send_timeout 15s;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For "";
proxy_set_header Forwarded "";
proxy_set_header X-Forwarded-Proto "";
proxy_set_header Connection "";

Keep access and body logging disabled and the backend bound to loopback. Do not add wildcard CORS headers: the Rust service already answers the allowed app origin. An nginx reload is sufficient for a route fix; restarting the intent service alone cannot fix a route nginx rejects. If validation fails, restore the saved configuration and validate it before reloading.

These read-only checks need no payment, wallet or transaction:

curl --max-time 10 -i -H 'Origin: https://app.sealpool.app' \
  'https://intent.sealpool.app/relay/info'
curl --max-time 10 -i -H 'Origin: https://app.sealpool.app' \
  'https://intent.sealpool.app/archive/health'
curl --max-time 10 -i -H 'Origin: https://app.sealpool.app' \
  'https://intent.sealpool.app/archive/headers?after=0&limit=100'
curl --max-time 10 -i -X OPTIONS \
  -H 'Origin: https://app.sealpool.app' \
  -H 'Access-Control-Request-Method: POST' \
  -H 'Access-Control-Request-Headers: content-type,x-seal-work' \
  'https://intent.sealpool.app/archive/batch'

/relay/info must keep the existing chain, pool, executor and submission key. /archive/health must return JSON from the Rust service; healthy storage reports readable: true and writable: true with HTTP 200. /archive/headers must return HTTP 200 with entries, next and hasMore; an empty entries array is valid. Each allowed-origin response must include Access-Control-Allow-Origin: https://app.sealpool.app, and the OPTIONS response must permit POST with the requested headers. An nginx HTML 404 without that header means the route is missing, not a wallet connection error. Compare with the same routes on http://127.0.0.1:8789 from the host: loopback success with a public nginx 404 means the edited server block is not the active one; a Rust JSON 503 means the route works but delivery storage needs investigation.

A missing schema reports delivery unavailable while the intent service's immediate execution path remains available. /healthz checks HTTP service liveness; /archive/health checks storage readability and write headroom independently. See Transfer delivery for fallback, retention and tests.

Testnet and later production networks

Initialization requires an explicitly selected, installed mock Sepolia run and starts in simulate mode. Review the deployment and change its configuration to test-subsidy before signing with test ETH. The initial limits are 0.1 test ETH per transaction and 0.2 per UTC day. Mock test-token fees cannot establish real profitability. Initialization does not enable mainnet signing.

The native engine also supports Ethereum L1 and Base profitability policies: validated prices, fee collection costs, a profit floor, budget limits and, on Base, L1/operator gas and sequencer checks. Those deployments still need their own reviewed contract configuration, price feeds and funded executor before activation. make init does not automatically enable mainnet chains.

Frontend and health

A deployed frontend uses the HTTPS origin in its relayerUrl, such as https://intent.sealpool.app. It discovers the public encryption key there and verifies the chain/pool binding. TLS authenticates this discovery; a separately pinned submission key is not used when discovery succeeds. Replacing an executor for the same pool and endpoint therefore needs no frontend rebuild. A new pool requires matching frontend deployment configuration. Reload the UI after restarting the intent service. The browser-only sandbox does not send intent service transactions.

curl --fail https://intent.sealpool.app/healthz
curl --fail https://intent.sealpool.app/relay/info

Health indicates the HTTP service is running; the printed balance and the per-intent policy checks determine whether it can execute. Confirm that /relay/info reports the expected pool, adapters and operations before using swaps or private margin. Publishing the website does not restart this service.