Protocol Integration Guide
How a protocol integrates Final Wallet: recognising wallets, ERC-1271 signatures, controller-bound sub-accounts, and what never to assume.
Last updated 2026-09-08 · maintained in the Final Wiki
Both the Pre-Final Chain and Final Chain are fully EVM-compatible: your contracts, tooling and integrations carry over unchanged, whether you move, adapt, or only interface with us. Only strictly sequential parts of a protocol behave differently under Final Chain's asynchronous model, and a dedicated lane lets them persist their ordering.
For teams whose contracts will be called by Final Wallets and sub-accounts, or who want a dedicated, policy-bound sub-account for their protocol. This page is about the surfaces your contracts see. It does not describe how the protocol carries intents internally; you do not need that to integrate.
#1. What calls you
Every Final Wallet action reaches your contract as an ordinary call whose msg.sender is either:
- the wallet address (stable, identical on every chain), or
- one of its sub-accounts (a wallet-owned custody address, stable per route).
Both are smart contracts. Neither is an EOA. The call may originate from a bundle carrying many users' intents, but each call is authorized per intent by that wallet's holder, and your contract sees one caller per call as usual.
Consequences for your code
- Never gate on
tx.origin,extcodesize == 0, or "is this an EOA". - Roles and ownership you grant must point at the wallet or sub-account address that should hold them, never at a key.
- Do not assume the caller cannot receive a callback synchronously, the way an EOA cannot; the wallet can, and sub-accounts can, within the rules below.
#2. Recognising a Final Wallet or sub-account
The chain-global wallet factory (same address on every chain) exposes two views:
isFinalWallet(address) → boolisFinalSubAccount(address) → bool
Both are dual checks (registry and runtime code hash), so they authenticate; an unregistered address carrying the proxy runtime is refused. Use them to offer Final-specific behaviour, never to grant privileges: authorization is always the caller's own.
Addresses of the factory and the gateway per chain are published in the API's GET /system/config.
#3. Signatures: ERC-1271
A Final Wallet validates signatures through ERC-1271 isValidSignature(hash, signature). Support it wherever your protocol accepts signatures (permits, orders, off-chain approvals). Notes:
- A wallet in post-quantum mode returns success only for hashes the wallet has explicitly approved on chain. Off-chain signatures in the classical sense are not produced by PQ wallets; design permit-style flows so that an on-chain approval is an acceptable substitute.
- Sub-accounts are contract actors for ownership and role checks; they are not independent signature wallets unless your integration explicitly grows that surface.
- Guardian and recovery signatures are typed data only. If your protocol interacts with recovery, sign EIP-712 structures, never prefixed messages.
#4. Sub-accounts for your protocol
A sub-account is an isolated custody route owned by exactly one wallet. There are two kinds:
- Unbound: general-purpose isolation. The wallet executes through it; your contract sees the sub-account as caller.
- Controller-bound: the sub-account is bound to a controller contract, your protocol's policy. Execution through that sub-account is checked against your controller's rules.
Controller requirements
- A controller must be a service contract with code. A codeless controller bricks the sub-account (every controller call site reverts), so it is refused at binding.
- A controller must not be a Final Wallet or a sub-account: anything carrying the wallet or sub-account proxy runtime is rejected by all three gates (factory, sub-account, preflight).
- Policy changes go through the wallet's controller policy update request-and-accept flow. The holder must accept; your controller cannot change what the sub-account will do unilaterally.
- The holder always retains an escape: a maintenance path to unbind and recover custody, with a delay your policy cannot remove.
What binds you, from the holder's side — the wallet treats every controller as untrusted and bounded: you can spend at most what the holder budgeted into the one sub-account (never the wallet, never another route, never a token outside the budget); the lane is closed until the holder has set budgets; the holder's scope sits above your policy; and the holder can escape or rotate you at any time with no callback. Design flows so a refused token or an exhausted cap degrades your UX, never your solvency. A holder may also grant an agent — a bot or an AI agent — a bounded allowance on a sub-account bound to your controller; your policy still runs as the third check on every such dispatch. The holder-side view: Sub-accounts, controllers and agents.
Recommended shape for a protocol controller
- Define the allowed targets and selectors for the route.
- Define value and rate caps in native terms; the sub-account's own
AccountScope(time window, native cap, optional target allowlist) applies on top and is the holder's, not yours. - Emit an event on every accepted policy version so integrators can audit what changed.
#5. Intents and bundles: what you can rely on
- Each intent executes exactly once and in the wallet's nonce order.
- An intent may declare require success (its revert propagates instead of being swallowed), skip if a dependency failed, or an in-bundle dependency on another intent of the same wallet in the same bundle.
- A bundle may be atomic; agent grants can require atomic execution.
- Cross-chain dependencies are not expressed inside a bundle. If your flow spans chains, use the settlement rail's operations and observe completion events; do not infer a remote leg from a local intent's success.
#6. Fees and gas
Holders do not pay gas directly; the protocol's gateway executes bundles and charges fees from the wallet according to the published schedule. Your contract does not need to be gas-sponsored and receives no fee logic. If you want to sponsor your users' Final DeFi fees, the deposit path on the gateway (depositFor) prefunds a wallet's fee balance permissionlessly.
#7. Sub-account lifecycle you may observe
- Creation is deterministic per (wallet, route). Addresses are stable; you may precompute or read them from the factory once created.
- Funding a sub-account from its wallet, executing through it, and binding or unbinding a controller are all wallet actions and emit events on the wallet and sub-account.
- Dormant-funds routing and recovery apply to sub-accounts through their wallet; a protocol should never rely on a sub-account being immune to its owner's recovery outcome.
#8. Testing your integration
- Both testnets carry the full stack (Sepolia, OP Sepolia). Wallets there are created the same way as on mainnet.
- Use
POST /preflightto check that an intent to your contract would be admitted before asking a user to sign it. - The reference wallet tooling can create a controller-bound sub-account for your controller on a test wallet; ask us for a test identity if you need one.
#9. Checklist
- [ ] No EOA assumptions anywhere in the path a Final Wallet takes.
- [ ] ERC-1271 accepted where signatures are accepted; PQ-mode approvals handled.
- [ ] Roles granted to wallet or sub-account addresses.
- [ ] Controller has code, is not a wallet proxy, and versions its policy.
- [ ] Cross-chain flows use settlement operations, not in-bundle dependencies.