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
- The frontend requests a fee quote over HTTPS and encrypts its intent.
- The service authenticates the quote, validates the proof or controller signature, verifies the deployed contracts, and simulates the exact transaction onchain.
- 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.
- Valid intents declined because of price, budget, simulation-only policy or
insufficient ETH are saved encrypted in
seal_deferred.intentsusing the single supplied database URL. Invalid proofs and expired new submissions are rejected without insertion. - 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.