EIP-712 Meta-Transactions for a Gasless Launchpad: How THRYX Does It
10 min read
Most launchpad articles wave a hand at "gasless" and move on. This post is the opposite: it walks through the actual EIP-712 typed-data structures THRYX uses, the relay path the signed payload takes, the on-chain verification logic that prevents replays, and the paymaster loop that closes the economic loop so the relay does not bleed ETH. If you are building a launchpad, an account-abstraction wallet, or just an agent that needs to sign without holding gas — this is how the moving parts fit together.
What EIP-712 actually is
EIP-712 (finalized 2018, in production everywhere since the Permit standard popularized it) is a way to sign structured JSON data instead of an opaque hex blob. Pre-712, every signature looked like 0xabcd... in MetaMask and the user had to trust the dapp not to lie about what it represented. Post-712, the wallet displays the field names and values: { name: "MyToken", symbol: "MTK", deadline: 1735689600 }. That readability is not just UX — it is the security property that makes meta-transactions safe enough to ship.
A 712 signature is computed over four pieces concatenated and hashed: a fixed prefix (0x1901), a domain separator that pins the signature to a specific chain id + contract address + app name + version, a typed struct hash of the payload, and the keccak256 of all of that as the digest the user signs. The wallet renders the struct fields, the user clicks accept, and what comes back is an ECDSA signature over the digest. Critically: the contract that later verifies the signature must reconstruct the exact same digest from the exact same domain separator. If the chain id, contract address, or app name differs by one character, recovery returns a wrong address and the verification fails.
What a meta-transaction adds
A meta-transaction is just a signed message that authorizes a third party (the relay) to submit the underlying transaction on the signer's behalf. The relay pays the gas. The contract verifies the signature, recovers the original signer address, and treats the call as if that address had submitted it directly — but msg.sender inside the call is the relay, not the original signer.
That last detail is what makes the design tricky. Naive contract code uses msg.sender to identify the user. Meta-transaction-aware code has to use the recovered EIP-712 signer instead. THRYX handles this by passing the signer as an explicit parameter (creator, owner, beneficiary) inside every meta-call typehash, then asserting that the recovered signature matches that parameter. No EIP-2771 trusted-forwarder dance, no _msgSender() override — just an explicit signer field that the contract verifies against the signature.
The MetaLaunchV4 typehash, in full
Here is the actual typed-data struct THRYX uses today (post v3.0.2, 2026-05-04) for a V4-native token launch:
MetaLaunchV4(
string name,
string symbol,
string image,
address creator,
uint256 nonce,
uint256 deadline
)
And the EIP-712 domain that pins this to the THRYX Diamond on Base mainnet:
{
"name": "THRYX",
"version": "1",
"chainId": 8453,
"verifyingContract": "0x2F77b40c124645d25782CfBdfB1f54C1d76f2cCe"
}
Three things to notice. First, the creator field is the explicit signer — the contract will recover the signature, compare against this address, and revert on mismatch. Second, nonce is a per-user monotonically increasing counter the contract tracks in storage; submitting the same nonce twice is a revert. Third, deadline is a unix timestamp after which the signature is invalid; this prevents an old signature from being held in reserve and submitted weeks later. These three fields together are the standard meta-transaction replay-protection triple.
Signing it from the browser (or an agent)
With ethers v6 the signing call is short:
import { Wallet, TypedDataDomain } from "ethers";
const domain = {
name: "THRYX",
version: "1",
chainId: 8453,
verifyingContract: "0x2F77b40c124645d25782CfBdfB1f54C1d76f2cCe",
};
const types = {
MetaLaunchV4: [
{ name: "name", type: "string" },
{ name: "symbol", type: "string" },
{ name: "image", type: "string" },
{ name: "creator", type: "address" },
{ name: "nonce", type: "uint256" },
{ name: "deadline", type: "uint256" },
],
};
const value = {
name: "MyToken",
symbol: "MTK",
image: "ipfs://Qm...",
creator: signer.address,
nonce: await diamond.metaNonce(signer.address),
deadline: Math.floor(Date.now() / 1000) + 600, // 10 min
};
const signature = await signer.signTypedData(domain, types, value);
No on-chain call yet. No gas spent. The signer wallet does not need any ETH to produce this signature — it is a local cryptographic operation. The signer can be a hardware wallet, an in-memory key, a custodial key the THRYX server derives from the user's account, or an MCP-server key an external AI agent never even sees.
The relay path
The signed payload + signature goes to the THRYX relay — a Cloudflare Worker at thryx-relay.thryx.workers.dev. The relay is intentionally dumb: it accepts the signed JSON over HTTPS, validates the shape, attaches its own ETH-funded sender wallet, and submits the on-chain call to the Diamond facet that owns the matching selector. For MetaLaunchV4 that is LaunchV4Facet (0x1A3C1FF7786D15f3aFC3202E8728623DF956F9F0).
The relay wallet (0x888F4365eBcF38B6213dB489F68F66427E2E11B7) is registered on the Diamond as the paymasterAuthorizedCaller — set on-chain at block 43849451. The Diamond accepts meta-calls from any address (the EIP-712 signature is what matters), but having a known authorized caller makes accounting cleaner: every gas reimbursement flows back to that single address from the paymaster ETH balance.
On-chain verification, line by line
Inside LaunchV4Facet.metaLaunchV4, the verification flow is short and worth reading in plain language:
- require(block.timestamp <= deadline, "expired") — reject signatures that have aged out.
- bytes32 structHash = keccak256(abi.encode(METALAUNCHV4_TYPEHASH, keccak256(bytes(name)), keccak256(bytes(symbol)), keccak256(bytes(image)), creator, nonce, deadline)) — rebuild the typed-data struct hash.
- bytes32 digest = keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, structHash)) — apply the EIP-712 prefix.
- address recovered = ecrecover(digest, v, r, s) — pull the signer.
- require(recovered == creator && recovered != address(0), "bad sig") — assert it matches the creator field and is not the zero address (ecrecover returns 0 on bad inputs).
- require(metaNonces[creator] == nonce, "bad nonce") — enforce monotonic ordering per signer.
- metaNonces[creator] = nonce + 1 — burn the nonce so the signature can never be replayed.
- Then proceed with the actual launch logic, treating creator as msg.sender for accounting purposes.
Total verification overhead: roughly 20,000 gas on top of the underlying launch cost. On Base at ~0.01 gwei that is fractions of a cent. The cost is negligible relative to the UX win of letting users sign without holding ETH.
Why not EIP-2771 or ERC-4337?
EIP-2771 standardizes a "trusted forwarder" pattern: a contract trusts a single forwarder address to call into it with the original signer appended at the end of the calldata. ERC-4337 (account abstraction) goes further: a UserOperation flows through a global EntryPoint contract that handles signature verification, gas accounting, and bundler payment in one shared infrastructure layer.
Both standards are reasonable. THRYX uses neither, for two reasons. First, the explicit-signer approach used by 712 typed-data is simpler — there is no extra contract layer to audit, and the signer is a parameter the user actually sees in their wallet prompt. Second, ERC-4337 entry points add a real per-call gas tax (the bundler must execute validation in a separate frame, then the actual call, then the post-op refund). On Base that overhead is absorbable, but on a launchpad where every action is a meta-call, paying it 880 times for 880 launches and ~10x more for trades adds up. The 712 + custom relay path is leaner.
The trade-off: THRYX signatures are not portable to other 4337-compatible wallets. A user signing a MetaLaunchV4 on THRYX cannot easily reuse the same wallet abstraction on another protocol's entry point. That is fine for THRYX's scope. If you are building infrastructure rather than an app, ERC-4337 is the right answer; if you are building a vertical product, the simpler 712 approach often wins.
Closing the economic loop: paymaster reimbursement
A relay that pays gas forever runs out of ETH. The closing piece of the design is the on-chain paymaster that reimburses the relay out of protocol fees collected on every swap.
The flow: every swap on THRYX leaves a small ETH residual in the Diamond after the THRYX-side accounting closes. That residual rolls into the paymaster ETH balance inline. Separately, AutoBalanceFacet swaps protocol-reserve THRYX for ETH whenever the paymaster falls below a threshold (50M THRYX-equivalent, 100M target — set in v2.11). And PaymasterFacet exposes a topUpRelay() function (added v2.13, block 45182471) that any keeper can call permissionlessly to refill the relay wallet from paymaster ETH whenever the relay's gas runway dips below the configured target (currently 0.005 ETH).
The combined effect: the relay's ETH balance is auto-replenished from the paymaster, the paymaster is auto-replenished from protocol fees, and the protocol fees come from the 30% cut of every swap (the other 70% goes to creators). The whole loop is on-chain, autonomous, and has run without manual top-ups since v2.6. The only out-of-band cost is whatever the off-chain Cloudflare Worker hosting costs (a flat $0/mo at the volumes THRYX runs).
How a buy actually flows, end to end
Putting it all together, here is the complete path a gasless buy takes on THRYX. The user has $0.10 in ETH-equivalent THRYX in their portfolio and clicks Buy on a token page:
| Step | Where | Cost to user |
|---|---|---|
| 1. UI assembles MetaSwap struct | Browser | $0 |
| 2. User signs typed data | Browser wallet (custodial or self) | $0 |
| 3. Relay receives signed payload over HTTPS | Cloudflare Worker | $0 |
| 4. Relay submits on-chain tx with its own ETH | Base mainnet | $0 (relay pays) |
| 5. Diamond verifies EIP-712 signature + nonce + deadline | SwapFacet | ~20k gas |
| 6. Swap executes (curve or V4) | SwapFacet → curve / Uniswap V4 | ~250–350k gas |
| 7. ETH residual rolls into paymasterEthBalance | PaymasterFacet inline | inline |
| 8. Relay reimbursed next refill cycle | PaymasterFacet.topUpRelay() | permissionless keeper |
The user pays the swap fee (1% on new launches, 0.5% on the ~600 legacy tokens locked at the v2.14 grandfathered rate) and nothing else. Round-trip cost — buy and immediately sell back — works out to roughly 4.5% on new tokens and 3.5% on legacy at small trade sizes, which is the floor any strategy on the platform has to clear.
Security: replay, signature malleability, deadline games
A meta-transaction system is exactly as secure as its replay protection. THRYX's three-line defense is the standard one: (1) per-signer monotonic nonce stored on-chain in metaNonces[creator]; (2) deadline timestamp inside the typed data; (3) chain id pinned by the EIP-712 domain separator. Each of these blocks one class of replay:
- Same chain, same signature, same nonce → metaNonces[creator] has already advanced; revert.
- Same signature submitted weeks later → block.timestamp > deadline; revert.
- Same signature replayed on a forked chain (Base testnet, Optimism, etc.) → domain.chainId is wrong, ecrecover returns a different address; mismatch with creator; revert.
Signature malleability is handled by the standard ecrecover guard (reject s values above secp256k1n/2). The Diamond uses solady's ECDSA library which enforces this by default. The deadline window is a relay policy choice — THRYX UI defaults to 10 minutes, which is long enough to absorb network reorgs but short enough that a stolen signature is mostly useless by the time anyone could exploit it.
What this enables for AI agents
The 712 + relay design generalizes cleanly to autonomous agents. THRYX exposes 21 MCP tools (Model Context Protocol) at thryx.fun/.well-known/mcp.json. An agent registers via a single curl call, receives a custodial wallet derived from its account, and from then on every tool call that mutates state under the hood produces an EIP-712 signature against that wallet, sends it to the relay, and gets a transaction receipt back. The agent never holds ETH. The agent never sees gas in any of its tool responses. The agent does not need to know the paymaster exists.
On top of that primitive, the per-user on-platform autotrader at thryx.fun/autotrader runs Llama 3.3 70B via Groq, ticks every 60 seconds, and decides whether to buy, sell, broadcast, or skip. Every trade it submits goes through the same MetaSwap path a manual user uses — the autotrader has no special privilege beyond being able to mint a short-lived JWT for the user it represents. That symmetry is the whole point: the meta-transaction layer is the only thing the autotrader, an MCP-driven external agent, and a human in a browser have in common.
Common questions
Frequently asked
- What happens if the relay goes down?
- A user can sign the same EIP-712 payload and submit it on-chain themselves through any Base RPC. The Diamond does not require the relay path — it just requires a valid signature. If the relay is down, gasless UX is broken (the user has to pay their own gas), but the system is not censored. The relay is a convenience layer, not a privilege gate.
- Why use a custodial signing key for new accounts instead of forcing wallet connection?
- Friction. Most THRYX users sign up with email and password. The server derives a deterministic signing key per account and uses it to produce EIP-712 signatures on the user's behalf when the user clicks Buy or Launch. Users who prefer self-custody can connect a real wallet (MetaMask, Rabby, etc.); the same code path works for both. The custodial key is what enables the ~5-second signup-to-first-trade flow.
- How does the contract know which signer corresponds to which custodial user?
- It does not, and it does not need to. From the contract's perspective the recovered signature address is the user. The server-side mapping from email account → custodial key → on-chain address is bookkeeping outside the contract. On-chain there is no concept of "email user" — there are only addresses and the nonces and balances they own.
- Are there limits on how many meta-transactions can be queued for one signer?
- The nonce is monotonically increasing, so transactions per signer are sequential. The relay submits them one at a time in nonce order. There is no on-chain queue limit, but practically the relay batches by submitting one tx per block per signer to avoid mempool ordering issues.
- What if a malicious relay swaps in a different deadline or modifies my struct?
- It cannot. Any modification to the typed-data fields changes the digest, which changes ecrecover's output, which fails the require(recovered == creator) check inside the facet. The only thing the relay controls is whether to submit the signed payload at all — it cannot alter what was signed.
- Can the same signature be reused on a different facet on the same Diamond?
- No. The typehash includes the struct name (MetaLaunchV4), so a signature for MetaLaunchV4 cannot be replayed against MetaSwap or any other facet — the digests differ. Each meta-call has its own typehash and its own validation path inside the facet that owns its selector.
See the THRYX agent integration docs