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-txAuthentication
Include your API key in the request headers:
apikey: YOUR_API_KEYOverview 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:
approve()- Authorize a spender to use your tokenstransferFrom()- 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
// 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 { 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:
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:
{
"txId": "68fa3450539a3c9d28bbca33"
}Environment Variables
The following environment variables are used in the example:
| Variable | Description |
|---|---|
RPC_URL | JSON-RPC endpoint for the target chain |
PRIVATE_KEY | Private key used to create the EIP-712 signature (in custodial setups this may be held in a secure signer) |
PERMIT_SWAP_CONTRACT | Address of the contract exposing permitAndSwap |
TOKEN_ADDRESS | Token to be permitted and swapped |
TACHYON_API_KEY | Tachyon relay API key |
Workflow Summary
- Create EIP-712 Signature: User signs a permit message off-chain
- Encode Function Call: Combine the signature with swap parameters
- Submit to Relay: POST the encoded transaction to
/api/submit-tx - Track Execution: Monitor the transaction using the returned
txId
Benefits of Permit-Based Swaps
- Gasless for Users: Users don’t need native tokens to approve token spending
- Single Transaction: Approval and swap happen atomically
- Better UX: No need for separate approval transactions
- Custodial Friendly: Works seamlessly with custodial wallet architectures
- Security: EIP-712 provides clear, human-readable signing context
Best Practices
- Set Reasonable Deadlines: Use appropriate expiration times for permit signatures (e.g., 1 hour)
- Validate Token Support: Ensure tokens implement the ERC-2612 permit extension
- Handle Nonces Correctly: Always fetch the current nonce before creating signatures
- Secure Private Keys: In production, use secure key management solutions
- 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:
- Documentation: https://docs.tachyon.rath.fi
- Support: [email protected]
- GitHub Examples: https://github.com/RathFinance/tachyon-examples