Actions & Signing

Every on-chain action follows the same Universal Action Pattern: you sign an EIP-712 intent, sign a gas transaction that is cryptographically bound to that intent, and submit them together. The backend verifies the binding, broadcasts the gas payment, and executes the action.

The pattern

  1. Sign the intent — EIP-712 typed data for the action → intentSignature.
  2. Sign a gasTx — a raw EIP-1559 ETH transfer to the executor wallet. Its data field must equal keccak256(intentSignature). This binds the ETH payment to that exact intent.
  3. Sign an assetTx (deploy only, when initialBuyAmount > 0) — a raw ERC-20 transfer of the counter asset to the executor wallet, at nonce gasTx + 1.
  4. Sign an actionTx (liquidity actions and claimRewards) — the pre-signed contract call the backend relays, at nonce gasTx + 1.
  5. Submit everything to /execute/:action (or /verify/:action for a dry run).

Intent binding is mandatory

gasTx.data must be keccak256(intentSignature) for every action. Requests without this binding are rejected.

EIP-712 domain

Read the live values from GET /infoeip712Domain and eip712Types. The domain is:

{
  "name": "Fren Agent Protocol",
  "version": "1",
  "chainId": 4663,
  "verifyingContract": "0x0000000000000000000000000000000000000000"
}

Transaction fields per action

| Action | gasTx | assetTx | actionTx | |--------|---------|-----------|------------| | deploy | ETH fee | ERC-20 counter asset (if initialBuyAmount > 0) | — | | swap | ETH fee | — | signed Universal Router call | | addLiquidity | ETH fee | — | signed position-manager call | | increaseLiquidity | ETH fee | — | signed position-manager call | | removeLiquidity | ETH fee | — | signed position-manager call | | collectFees | ETH fee | — | signed position-manager call | | withdrawFees | ETH fee | — | — | | claimRewards | ETH fee | — | signed DeployHandler.release call |

Nonce rule: gasTx at nonce N; assetTx or actionTx at nonce N+1.

Field names

Preferred names are gasTx and assetTx. The older feeTx and counterAssetTx are still accepted for backward compatibility.

Fees

Each action reads its own fee from on-chain protocol config (deploy, lp, withdraw, claim are independent). Never hardcode them — read GET /infofees. Values are human strings like "0.000003949975364004 ETH":

const feeStr = '0.000003949975364004 ETH'
const wei = parseUnits(feeStr.split(' ')[0], 18)

Verify, then execute

Always dry-run with /verify/:action (same body as execute). It checks the signature, fee, deadline, and parameters without spending anything.

curl -X POST https://api.fren.gg/api/robinhood/agent/verify/deploy \
  -H "Authorization: Bearer {your-api-key}" \
  -H "Content-Type: application/json" \
  -d '{ "intent": { ... }, "intentSignature": "0x...", "gasTx": "0x..." }'

Failed actions still cost gas

/execute broadcasts the gasTx before the action runs. If the action reverts (bad params, insufficient balance, contract error), the gas fee is consumed and unrecoverable. Verify first, every time.

Buying and selling

Trading happens directly on canonical Uniswap V4 — there is no backend-signed buy/sell action. Quote with GET /swaps/quote, build calldata with POST /swaps/build-tx, then either submit the Universal Router transaction from your own wallet (approve the input token to Permit2, then Permit2 to the Universal Router first), or use the swap relay action: sign the Universal Router tx yourself and include it as actionTx. The backend validates the sender, nonce, and intent binding, checks the tx targets the Universal Router, and broadcasts it — it never signs your swap and never custodies funds.

SwapIntent:

{ tokenIn: address, tokenOut: address, fee: uint24,
  amountIn: uint256, minAmountOut: uint256, deadline: uint256 }

There is no recipient field: V4 swaps route through the Universal Router, whose TAKE_ALL leg pays the router caller. The relay forces the swap sender to be the intent signer, so output always lands on the signer — it cannot be redirected.

Constraints: minAmountOut > 0 (never sign an unbounded swap); actionTx must target the Universal Router and its calldata must be execute(commands, inputs, deadline) carrying exactly one V4_SWAP command whose action sequence is SWAP_EXACT_IN_SINGLE → SETTLE_ALL → TAKE_ALL, with the pool key (fee, tickSpacing=200, no hooks), swap direction, amounts (amountOutMinimum >= minAmountOut), and deadline all matching the signed intent — drifted calldata is rejected; nonce rule gasTx at N, actionTx at N+1.

Deploy

DeployIntent:

{ name: string, symbol: string, creator: address, feeManager: address,
  counterAsset: uint8, initialBuyAmount: uint256, minTokensOut: uint256,
  deadline: uint256 }

Constraints (validated server-side and on-chain):

  • A logo is mandatory — upload it first via POST /upload-logo, then pass the returned URL as metadata.logoUrl.
  • initialBuyAmount must be within GET /infodeploy.initialBuyBounds for your counter asset, and be greater than 0.
  • minTokensOut must be greater than 0 when initialBuyAmount > 0.
  • counterAsset: 0 = WETH, 1 = USDG, 2 = FREN.
curl -X POST https://api.fren.gg/api/robinhood/agent/execute/deploy \
  -H "Authorization: Bearer {your-api-key}" \
  -H "Content-Type: application/json" \
  -d '{
    "intent": {
      "name": "My Token",
      "symbol": "MTK",
      "creator": "0xYourWalletAddress",
      "feeManager": "0xYourWalletAddress",
      "counterAsset": 0,
      "initialBuyAmount": "10000000000000000",
      "minTokensOut": "1",
      "deadline": 1771834000
    },
    "intentSignature": "0x...",
    "gasTx": "0x...",
    "assetTx": "0x...",
    "metadata": {
      "logoUrl": "https://logos.fren.gg/logos/robinhood/agent-upload-.../original.png",
      "website": "https://example.com",
      "twitter": "@handle"
    }
  }'

Response: { gasTxHash, assetTxHash, action, agentAddress, transactionHash, tokenAddress, positionId, blockNumber }.

Liquidity actions

Liquidity uses a relay pattern: you sign the actual NonfungiblePositionManager call yourself and pass it as actionTx; the backend broadcasts it after the gasTx. Applies to addLiquidity, increaseLiquidity, removeLiquidity, and collectFees.

curl -X POST https://api.fren.gg/api/robinhood/agent/execute/addLiquidity \
  -H "Authorization: Bearer {your-api-key}" \
  -H "Content-Type: application/json" \
  -d '{
    "intent": {
      "token0": "0xLowerAddress",
      "token1": "0xHigherAddress",
      "fee": 10000,
      "tickLower": -887200,
      "tickUpper": 887200,
      "amount0Desired": "1000000000000000000000",
      "amount1Desired": "100000000000000",
      "amount0Min": "0",
      "amount1Min": "0",
      "recipient": "0xYourWalletAddress",
      "deadline": 1771834000
    },
    "intentSignature": "0x...",
    "gasTx": "0x...",
    "actionTx": "0x..."
  }'

token0 must be the numerically lower address. Fee tiers: 100, 500, 3000, 10000. collectFees has no deadline field — use max uint128 (340282366920938463463374607431768211455) for amount0Max/amount1Max to collect everything.

Withdraw fees

Collects the Uniswap V3 LP fees accrued to the locked position of a token you deployed. No actionTx — just intent + gasTx.

# Check what is claimable first
curl https://api.fren.gg/api/robinhood/agent/claimable-fees/0xYourFeeManagerAddress \
  -H "Authorization: Bearer {your-api-key}"
 
curl -X POST https://api.fren.gg/api/robinhood/agent/execute/withdrawFees \
  -H "Authorization: Bearer {your-api-key}" \
  -H "Content-Type: application/json" \
  -d '{
    "intent": { "token": "0xTokenAddress", "recipient": "0xYourFeeManagerAddress" },
    "intentSignature": "0x...",
    "gasTx": "0x..."
  }'

Claim rewards

Token deployers earn protocol rewards via DeployHandler contracts. The actionTx is a signed DeployHandler.release(token) call.

# Check available rewards
curl https://api.fren.gg/api/robinhood/fren-rewards/wallet/0xYourWalletAddress
 
curl -X POST https://api.fren.gg/api/robinhood/agent/execute/claimRewards \
  -H "Authorization: Bearer {your-api-key}" \
  -H "Content-Type: application/json" \
  -d '{
    "intent": { "handler": "0xDeployHandlerAddress", "token": "0xTokenAddress" },
    "intentSignature": "0x...",
    "gasTx": "0x...",
    "actionTx": "0x..."
  }'

See the Agent Endpoints reference for the full request/response shapes.