@megaeth-labs/wallet-merchant
v0.1.4
Published
Server-side merchant (sponsor) for MegaETH wallet's wallet_sendCalls flow.
Maintainers
Keywords
Readme
@megaeth-labs/wallet-merchant
Server-side merchant (gas sponsor) for the MegaETH wallet's sponsored
wallet_sendCalls flow. The wallet builds the unsigned V0.6 intent locally
(it owns the chain client) and POSTs it to the merchant; the merchant
overrides payer + paymentMaxAmount, signs the gas-independent
PaymentAuthorization digest, embeds the signature, recomputes
txEntryGas / gasFloor / digest, and returns the result. The wallet then
adds the user's signature over the new digest and submits to the relay.
The merchant never reads chain state — every input it needs (intent fields, assembled calls, EIP-712 domain, and the user's signing key type) is in the request body.
Install
pnpm add @megaeth-labs/wallet-merchantFor local development the consumer (wallet-backend) uses a file: dep:
{
"dependencies": {
"@megaeth-labs/wallet-merchant": "*"
}
}Prerequisites
address must be a porto-upgraded (EIP-7702) merchant account. If key is the
merchant account's own secp256k1 root key, the signer emits a raw root
signature. If key is a session key, P256 key, or non-root secp256k1 key, it
must be authorized on the merchant account via a prior authorize(...) call;
the signer emits the porto-wrapped form (sig + keyHash + prehash), and the
orchestrator must be able to find keyHash in the merchant account's
keyStorage. Production setups should use a session key with spend permission
for the payment token and amount the merchant is willing to cover.
Usage
import { Hono } from 'hono';
import { merchant } from '@megaeth-labs/wallet-merchant';
const router = new Hono();
router.route(
'/',
merchant({
address: process.env.MERCHANT_ADDRESS as `0x${string}`,
// Plain hex defaults to secp256k1.
key: process.env.MERCHANT_PRIVATE_KEY as `0x${string}`,
}),
);P256 keys are also accepted, matching porto's merchant.Options.key:
merchant({
address: process.env.MERCHANT_ADDRESS as `0x${string}`,
key: {
type: 'p256',
privateKey: process.env.MERCHANT_PRIVATE_KEY as `0x${string}`,
},
});When paymentMaxAmount is omitted, the merchant derives its own cap from the
wallet-provided paymentPerGas and ESTIMATED_GAS_BUDGET. This avoids
retaining the wallet SDK's generic default cap, which is too large for
lower-decimal fee tokens. Set paymentMaxAmount only when you want a fixed
merchant spend cap, or set paymentGasBudget to tune the derived cap.
Funding a merchant key
To generate fresh merchant credentials and fund the new account, run this from a checkout of this repository:
pnpm create-merchant \
--network testnet \
--funder-key <0x...>The script always generates a new secp256k1 EOA. It transfers native ETH
and USDm from the --funder-key account to that new address, then uses
@megaeth-labs/wallet-intent to submit the 7702 upgrade through the relay.
USDm is sent through ERC20 0x15e9f2b0a747ac05c7446559306687085d161e5c.
If the funder account is already delegated, funding is submitted through the
relay as a single bundled call to avoid delegated-account in-flight raw
transaction limits when the delegation target matches the current relay account
proxy. Older or otherwise incompatible delegated funders fall back to serialized
raw transactions with retry.
On success the script prints MERCHANT_ADDRESS / MERCHANT_PRIVATE_KEY.
Flags:
--network testnet|mainnet— selects chain ID + RPC preset (testnet:6343/https://carrot.megaeth.com/rpc, mainnet:4326/https://mainnet.megaeth.com/rpc).--funder-key <0x...>— secp256k1 private key for the funding account.--merchant-key <0x...>with--upgrade-only— resume a run after funding succeeded but the 7702 upgrade failed.--eth-amount <amount>— native ETH amount to transfer. Defaults to0.001.--usdm-amount <amount>— USDm amount to transfer. Defaults to10.--chain-id <n>/--rpc-url <url>— override the preset.--relay-url <url>— override the preset relay URL (testnet:https://carrot.megaeth.com/relay, mainnet:https://mainnet.megaeth.com/relay).--write-env— append the credentials to./.env(use--forceto overwrite existing merchant credentials).
The script does not call the merchant service; it uses the same wallet-intent relay path as the wallet.
Demo service
Add merchant credentials to .env:
MERCHANT_ADDRESS=0x...
MERCHANT_PRIVATE_KEY=0x...Run a minimal local Hono service that mounts the merchant route at /.
pnpm demo builds the package, loads .env, and starts a PM2 process named
wallet-merchant-demo:
pnpm demoThe service listens on http://localhost:3000/ by default:
curl http://localhost:3000/Set PORT if you need something other than 3000:
PORT=4319 pnpm demoStop the local demo when finished:
pnpm exec pm2 delete wallet-merchant-demoThe same demo/index.ts file is also the Vercel Hono entrypoint. It exports
the app for Vercel and only starts the local Node listener when VERCEL is
not set. Vercel reads MERCHANT_ADDRESS and MERCHANT_PRIVATE_KEY from the
project environment variables; do not commit those values.
Vercel deployment
The Vercel project is configured by vercel.json to use the Hono runtime with
demo/index.ts as the entrypoint. Validate the production build locally:
vercel build --prod --yes --scope mega-ethDeploy the prebuilt output:
vercel deploy --prebuilt --prod --scope mega-ethThe production alias is:
https://wallet-merchant.vercel.app/Wire format
POST / with JSON body:
{
intent: JsonRpcIntent; // wallet-built unsigned intent
domain: { name; version; chainId; verifyingContract }; // orchestrator EIP-712 domain
keyType?: 'address' | 'p256' | 'secp256k1' | 'webauthn-p256';
}The bundled calls are decoded from intent.executionData on the merchant
side, so the wallet does not send them separately. keyType is used to mirror
wallet-side signature-verification gas buffers, including the WebAuthn buffer.
Successful response (200):
{ intent: JsonRpcIntent; digest: Hex }Errors are returned as { error: { code: number; message: string } } with a
4xx/5xx status (400 invalid request, 403 sponsor declined, 500 internal).
Options
type MerchantOptions = {
/**
* Porto-upgraded merchant account address, used as `intent.payer`.
* With a root-key setup, this is the EOA derived from `key`; with an
* admin/session-key setup, this is the upgraded EOA that authorized `key`.
*/
address: `0x${string}`;
/**
* Merchant key used to sign `paymentSignature`. If this is the merchant
* account's root secp256k1 key, the route emits a raw root signature.
* Otherwise, the key must be registered on `address` (via
* `authorize(...)`) before the route can produce valid wrapped signatures.
* When this is a session key, it must have spend permission for the
* payment token and covered amount.
*
* Accepts a `Hex` private key (treated as secp256k1) or
* `{ type: 'secp256k1' | 'p256'; privateKey: Hex }` for explicit
* selection, mirroring porto's `merchant.Options.key`.
*/
key:
| `0x${string}`
| { type: 'secp256k1' | 'p256'; privateKey: `0x${string}` };
/** Decide per-request whether to sponsor. Defaults to `true`. */
sponsor?: boolean | ((request: SponsorRequest) => boolean | Promise<boolean>);
/**
* Max amount of `paymentToken` (raw base units) the merchant is willing
* to pay per request — the merchant's own spend cap. Overrides whatever
* the wallet sent. When omitted, the merchant derives a cap from
* `paymentGasBudget` and the wallet-provided `paymentPerGas`.
*/
paymentMaxAmount?: bigint;
/**
* Gas budget used to derive the default merchant spend cap when
* `paymentMaxAmount` is omitted. Defaults to `ESTIMATED_GAS_BUDGET`.
*/
paymentGasBudget?: bigint;
/** Hono base path. Defaults to `/`. */
basePath?: string;
/** Hono CORS options. */
cors?: Parameters<typeof import('hono/cors').cors>[0];
};