Skip to Content
TachyonUse CasesSwap Using Custodial Wallets

Token Swap with Permit API Reference

This guide demonstrates how to perform token swaps using custodial wallets by leveraging EIP-712 signatures and ERC-20 permit functionality through the Tachyon relay API.


Endpoint

POST https://api.tachyon.rath.fi/api/submit-tx

Authentication

Include your API key in the request headers:

apikey: YOUR_API_KEY

Overview by Chain

EVM Chains (Ethereum, Base, Polygon, etc.)

Understanding EIP-712 Signatures

EIP-712 is a standard for typed structured data hashing and signing. It provides a more secure and user-friendly way to sign data compared to traditional message signing methods. Key benefits include:

  • Human-readable signatures: Users can see exactly what they’re signing in their wallet interface
  • Domain separation: Prevents signature replay attacks across different applications and chains
  • Type safety: Ensures the data structure matches expected formats

EIP-712 signatures are particularly useful for meta-transactions and gasless interactions, as they allow users to authorize actions without directly submitting transactions to the blockchain.

Reference: EIP-712 Specification 

ERC-20 Permit Functionality

The ERC-20 Permit extension (EIP-2612) enables token approvals via signatures instead of on-chain transactions. Traditional token approvals require two transactions:

  1. approve() - Authorize a spender to use your tokens
  2. transferFrom() - The spender moves the tokens

With permit functionality:

  • Users sign an off-chain message (EIP-712) authorizing token spending
  • The signature can be submitted by anyone (including relayers)
  • Approval and transfer happen in a single transaction
  • No gas costs for the token owner

This is ideal for custodial wallets and gasless experiences, as users can authorize token movements without holding native tokens for gas fees.

Reference: EIP-20 Permit Guide 


Permit and Swap Contract

contract.sol
// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; contract PermitSwap { address public swapper; constructor(address _swapper) { swapper = _swapper; } function permitAndSwap( address owner, address tokenIn, uint256 amountIn, bytes memory data, uint256 deadline, uint8 v, bytes32 r, bytes32 s ) external { IERC20Permit(tokenIn).permit(owner, address(this), amountIn, deadline, v, r, s); IERC20(tokenIn).transferFrom(owner, address(this), amountIn); IERC20(tokenIn).approve(swapper, amountIn); (bool ok,) = swapper.call(data); require(ok, "swap failed"); } }

Implementation Example

Reference:

import-sdk.ts
import { ethers } from "ethers"; import { ChainId, Tachyon } from "@rathfi/tachyon"; import * as dotenv from "dotenv"; dotenv.config({ path: "ts-example/.env" }); // === CONFIG === const PERMIT_SWAP_ADDRESS = process.env.PERMIT_SWAP_CONTRACT!; const TOKEN_IN_ADDRESS = process.env.TOKEN_ADDRESS!; const SWAPPER_ADDRESS = "0x6352a56caadC4F1E25CD6c75970Fa768A3304e64"; // on base // Example encoded swap payload — replace this with your actual swap call data const SWAP_DATA = "0x"; // === ABI === const permitSwapAbi = [ "function permitAndSwap(address owner, address tokenIn, uint256 amountIn, bytes data, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external", ]; async function main() { // Step 1. Setup provider and signer for signing the permit const provider = new ethers.JsonRpcProvider(process.env.RPC_URL); console.log("Using RPC URL:", process.env.RPC_URL); const signer = new ethers.Wallet(process.env.PRIVATE_KEY!, provider); const owner = await signer.getAddress(); const token = new ethers.Contract( TOKEN_IN_ADDRESS, [ "function name() view returns (string)", "function nonces(address) view returns (uint256)", ], provider ); const name = await token.name(); const nonce = await token.nonces(owner); const chain = await provider.getNetwork(); const amountIn = 10; // usdc const deadline = Math.floor(Date.now() / 1000) + 3600; // 1 hour from now // Step 2. Create EIP-712 domain and message const domain = { name, version: "2", // hardcoded for USDC on base chainId: chain.chainId, verifyingContract: TOKEN_IN_ADDRESS, }; const types = { Permit: [ { name: "owner", type: "address" }, { name: "spender", type: "address" }, { name: "value", type: "uint256" }, { name: "nonce", type: "uint256" }, { name: "deadline", type: "uint256" }, ], }; const message = { owner, spender: PERMIT_SWAP_ADDRESS, value: amountIn, nonce, deadline, }; const signature = await signer.signTypedData(domain, types, message); const sig = ethers.Signature.from(signature); const { v, r, s } = sig; // Step 3. Encode function call data for permitAndSwap() const iface = new ethers.Interface(permitSwapAbi); const callData = iface.encodeFunctionData("permitAndSwap", [ owner, TOKEN_IN_ADDRESS, amountIn, SWAP_DATA, deadline, v, r, s, ]); // Step 4. Initialize Tachyon const tachyon = new Tachyon({ apiKey: process.env.TACHYON_API_KEY || "", }); // Step 5. Relay the transaction via Tachyon console.log("Relaying transaction via Tachyon..."); const txId = await tachyon.relay({ chainId: ChainId.BASE, // Adjust chain if not Base to: PERMIT_SWAP_ADDRESS, value: "0", // No native value needed gasLimit: "1000000", callData, }); console.log("Relay Tx ID:", txId); // Step 6. Wait for the transaction to execute const relayStatus = await tachyon.waitForPendingExecutionHash(txId); console.log("Transaction Status:", relayStatus); } main().catch((error) => { console.error(error); process.exitCode = 1; });

API Request Example

Once you have the encoded callData from the permit signature, submit it via the relay API:

submit-transaction.sh
curl -X POST https://api.tachyon.rath.fi/api/submit-tx \ -H "apikey: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "chainId": 8453, "to": "0xPermitSwapContractAddress", "value": "0", "gasLimit": "1000000", "callData": "0x...", "label": "Permit and Swap" }'

Response:

response.json
{ "txId": "68fa3450539a3c9d28bbca33" }

Environment Variables

The following environment variables are used in the example:

VariableDescription
RPC_URLJSON-RPC endpoint for the target chain
PRIVATE_KEYPrivate key used to create the EIP-712 signature (in custodial setups this may be held in a secure signer)
PERMIT_SWAP_CONTRACTAddress of the contract exposing permitAndSwap
TOKEN_ADDRESSToken to be permitted and swapped
TACHYON_API_KEYTachyon relay API key

Workflow Summary

  1. Create EIP-712 Signature: User signs a permit message off-chain
  2. Encode Function Call: Combine the signature with swap parameters
  3. Submit to Relay: POST the encoded transaction to /api/submit-tx
  4. Track Execution: Monitor the transaction using the returned txId

Benefits of Permit-Based Swaps

  1. Gasless for Users: Users don’t need native tokens to approve token spending
  2. Single Transaction: Approval and swap happen atomically
  3. Better UX: No need for separate approval transactions
  4. Custodial Friendly: Works seamlessly with custodial wallet architectures
  5. Security: EIP-712 provides clear, human-readable signing context

Best Practices

  1. Set Reasonable Deadlines: Use appropriate expiration times for permit signatures (e.g., 1 hour)
  2. Validate Token Support: Ensure tokens implement the ERC-2612 permit extension
  3. Handle Nonces Correctly: Always fetch the current nonce before creating signatures
  4. Secure Private Keys: In production, use secure key management solutions
  5. Test Thoroughly: Test with small amounts on testnets before mainnet deployment

Rate Limits

API requests are subject to rate limits based on your subscription tier. Contact support for information about your specific limits.


Support

For questions or issues with the API, please contact:

Last updated on