Connecting to ETHGas
TestNet (Hoodi) and MainNet. Use TestNet first.
Prerequisites
- API Credentials: You'll need to register an account on ETHGas Exchange
- Environment Selection: Choose between TestNet (Hoodi) or MainNet
- Network Access: Ability to connect to ETHGas endpoints
Development Workflow
✅ Start with TestNet - Develop and test your integration
✅ Validate Functionality - Ensure all features work correctly
✅ Test with Real Data - Use TestNet's simulated real data
✅ Deploy to MainNet - Move to production when ready
The JWT access token is valid for 1 hour; after each hour, a refresh is required.
Include the JWT in the request header: Authorization: 'Bearer accessToken'.
A private session is valid for 7 days; after 7 days, re-login is required.
For WebSocket, include the access token in the session header: Bearer accessToken.
Authentication Workflow
For interacting with /v1 endpoints, a login is required. The login workflow is as follows: See lang tabs
-
Get Login Message: Call the endpoint
/v1/user/loginwith the appropriate addr to get the login sign message. -
Sign and Verify: Sign the login message and call the endpoint
/v1/user/login/verifywith the addr, nonceHash, and signature. You will receive the JWT access token, as well as user-related data, and can now call any private endpoints. -
Refresh Token: Before the access token expires, call
/v1/user/login/refreshto get a new access token.
Connecting to ETHGas
- General
- Using Python
- Using JavaScript
- Using Rust
- Using Go
-
Get Login Message: Call the endpoint
/v1/user/loginwith the appropriate addr to get the login sign message. -
Sign and Verify: Sign the login message and call the endpoint
/v1/user/login/verifywith the addr, nonceHash, and signature. You will receive the JWT access token, as well as user-related data, and can now call any private endpoints. -
Refresh Token: Before the access token expires, call
/v1/user/login/refreshto get a new access token.
Overview
The python-ethgas package is a Python3 connector that allows you to interact with the EthGas. The package utilizes threads to handle concurrent execution of REST API requests and WebSocket subscriptions. This allows for efficient handling of multiple requests and real-time data streams without blocking the main execution thread.
The connector provides a REST client that allows you to make requests to all the available REST API endpoints of the EthGas. It also includes a WebSocket client that enables you to subscribe to real-time data streams from EthGas. You can subscribe to channels like preconf market data, transaction data etc.
To access private endpoints and perform actions on behalf of a user, both API and Websocket client classes handle the login process and manage the authentication using the JWT access token.
This is an unofficial Python project and should be used at your own risk. It is NOT affiliated with EthGas and does NOT provide financial or investment advice.
Installation
pip install python-ethgas
Example Usage
import os
from eth_account import messages
import json
import requests
from web3.auto import w3
# ETHGAS_API_URL must include /api (e.g. https://mainnet.app.ethgas.com/api)
domain = os.environ.get("ETHGAS_API_URL", "https://mainnet.app.ethgas.com/api")
account_address = 'ADD_YOUR_ETHEREUM_account_ADDRESS_HERE'
private_key = 'ADD_YOUR_PRIVATE_KEY_HERE'
chain_id = 'ADD_THE_CHAIN_ID_HERE'
###########################################
# STEP 1. Get the login sign message. #
###########################################
# Login
body = {'addr': account_address}
response = requests.post(domain + '/v1/user/login', data=body)
print(response.status_code)
print(response.text)
# Retrieve nonce & response message
nonce = response.json()['data']['nonceHash']
eip712_message = json.loads(response.json()['data']['eip712Message'])
print(eip712_message)
# Make signature
encoded_message = messages.encode_typed_data(full_message=eip712_message)
# Sign message
signed_message = w3.eth.account.sign_message(encoded_message, private_key=private_key)
# Verify login
body = {'addr': account_address, 'nonceHash': nonce, 'signature': w3.to_hex(signed_message.signature)}
response = requests.post(domain + '/v1/user/login/verify', data=body)
# Was login successful?
print(f"Login successful? {response.json()['success']}")
#####################################
# STEP 2. Get the access token. #
#####################################
# Get access token etc
access_token = response.json()['data']['accessToken']['token']
headers = {'Content-Type': 'application/json', 'Authorization': 'Bearer ' + access_token}
response_cookies = response.cookies
# Test calling a private endpoint
response = requests.get(url=domain + '/v1/user/accounts', headers=headers)
print("Here is your list of Ethgas accounts etc:\n")
print(json.dumps(response.json()['data']['accounts'], indent=2))
###########################################################
# STEP 3. Refresh the access token before it expires. #
###########################################################
# Refresh token (obtained from verify response cookies)
refresh_token = response.cookies.get("refreshToken") or "ADD_YOUR_REFRESH_TOKEN_HERE"
body = {'refreshToken': refresh_token}
response = requests.post(domain + '/v1/user/login/refresh', cookies=response_cookies, data=body)
# Get latest access token etc
access_token = response.json()['data']['accessToken']['token']
headers = {'Content-Type': 'application/json', 'Authorization': 'Bearer ' + access_token}
response_cookies = response.cookies
# Test calling a private endpoint again
response = requests.get(url=domain + '/v1/user/accounts', headers=headers)
print("Here is your list of Ethgas accounts etc:\n")
print(json.dumps(response.json()['data']['accounts'], indent=2))
Repository: python-ethgas
Overview
The JavaScript connector allows you to interact with the ETHGas platform using modern JavaScript/TypeScript. This connector provides both REST API and WebSocket client capabilities for seamless integration with web applications and Node.js environments.
For accessing private endpoints and performing user-specific actions, the connector handles the complete authentication flow including login, JWT token management, and automatic token refresh.
Example Usage
const ApiClient = {
APIURL: config.exchange.endpoint,
ChainID: config.exchange.chainId,
AccessToken: null,
RefreshToken: null,
client: axios.create({
baseURL: config.exchange.endpoint,
timeout: 10000,
}),
};
async function login(privateKey) {
const maxRetries = 30;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const wallet = new ethers.Wallet(privateKey);
const address = wallet.address;
// Step 1: Login request
const formData = qs.stringify({
addr: address,
chainId: ApiClient.ChainID,
});
const loginResponse = await ApiClient.client.post("/v1/user/login", formData, {
headers: { "Content-Type": "application/x-www-form-urlencoded" },
});
if (!loginResponse.data.success) {
throw new Error("Login failed: response indicates failure");
}
const loginData = loginResponse.data.data;
const { eip712Message, nonceHash } = loginData;
// Step 2: EIP712 Signing
let type = JSON.parse(eip712Message).types;
delete type.EIP712Domain;
const signature = await wallet._signTypedData(
JSON.parse(eip712Message).domain,
type,
JSON.parse(eip712Message).message
);
const signatureHash = ethers.utils.hexlify(ethers.utils.arrayify(signature));
const verifyFormData = qs.stringify({
addr: address,
signature: signatureHash,
nonceHash,
});
const verifyResponse = await ApiClient.client.post("/v1/user/login/verify", verifyFormData, {
headers: { "Content-Type": "application/x-www-form-urlencoded" },
});
if (!verifyResponse.data.success) {
throw new Error("Verification failed: response indicates failure");
}
const verifyData = verifyResponse.data.data;
ApiClient.AccessToken = verifyData.accessToken.token;
ApiClient.RefreshToken = extractRefreshToken(verifyResponse);
return {
accessToken: ApiClient.AccessToken,
refreshToken: ApiClient.RefreshToken,
};
} catch (error) {
console.error(`Login attempt ${attempt + 1} failed:`, error.message);
if (attempt < maxRetries - 1) {
await delay(60000); // Wait for 1 minute before retrying
} else {
throw error;
}
}
}
}
async function refreshAccessToken() {
const maxRetries = 30;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const formData = qs.stringify({
refreshToken: ApiClient.RefreshToken,
});
const refreshResponse = await ApiClient.client.post("/v1/user/login/refresh", formData, {
headers: { "Content-Type": "application/x-www-form-urlencoded" },
});
if (!refreshResponse.data.success) {
throw new Error("Refresh token failed: response indicates failure");
}
const refreshData = refreshResponse.data.data;
ApiClient.AccessToken = refreshData.accessToken.token;
return;
} catch (error) {
console.error(`Refresh token attempt ${attempt + 1} failed:`, error.message);
if (attempt < maxRetries - 1) {
await delay(60000);
} else {
throw error;
}
}
}
}
Repository: ethgas-js
Overview
The Rust connector provides a high-performance, memory-safe way to interact with the ETHGas platform.
For private endpoint access, the connector implements the complete authentication workflow including EIP-712 message signing, JWT token management, and automatic token refresh.
impl PreconfApiClient {
pub async fn get_headers(&self) -> HeaderMap {
let mut headers = HeaderMap::new();
let guard = self.state.access_token.read().await;
let access_token = guard.clone();
if access_token.is_some() {
let token = access_token.clone().unwrap();
let authorization_header = format!("Bearer {}", token);
headers.insert(
AUTHORIZATION,
HeaderValue::from_str(authorization_header.as_str()).unwrap(),
);
}
headers.insert(USER_AGENT, HeaderValue::from_str("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36").unwrap());
headers
}
pub async fn re_login(&mut self) -> bool {
let mut retry_count = 0;
loop {
if self.login().await {
info!("ETHGas re-logged in.");
return true;
}
retry_count += 1;
if retry_count % 5 == 0 {
error!("Failed to login ETHGas, continue to retry login...");
}
}
}
pub async fn login(&mut self) -> bool {
let mut is_logged_in = false;
let secret_key_bytes = hex::decode(self.exchange_secret_key.as_str())
.expect("Failed to decode secret key for preconf login");
let signer: PrivateKeySigner = PrivateKeySigner::from_slice(secret_key_bytes.as_ref())
.expect("Failed to create signer from secret key for preconf login");
let address = format!("0x{:x}", signer.address());
let login_headers = self.get_headers().await;
let login_url = format!("{}/v1/user/login", self.api_url);
let login_response = match self
.client
.post(&login_url)
.headers(login_headers)
.form(&[("addr", address.clone())])
.send()
.await
{
Ok(response) => {
info!("Received the login response from ETHGas.");
response
},
Err(e) => {
let mut guard = self.state.health_status.write().await;
*guard = PreconfHealthStatus::ServerFailed;
error!("Failed to login ETHGas: {}", e);
return is_logged_in;
}
};
if login_response.status().is_success() {
if let ApiData::Login(login_resp) = login_response
.json::<ApiResponse>()
.await
.expect("Failed to decode ETHGas login response")
.data
{
if !login_resp.nonce_hash.is_empty() && !login_resp.eip712_message.is_empty() {
let eip712_msg: TypedData = serde_json::from_str(&login_resp.eip712_message)
.expect("Failed to parse EIP712 message into typed data");
let signature = signer
.sign_dynamic_typed_data(&eip712_msg)
.await
.expect("Failed to sign EIP712 message");
let signature_hex_str = format!("0x{}", hex::encode(signature.as_bytes()));
let verify_headers = self.get_headers().await;
// debug!("Generated signature: {}", signature_hex_str);
// Send the signature back to complete verification
let verify_response = match self
.client
.post(format!("{}/v1/user/login/verify", self.api_url))
.headers(verify_headers)
.form(&[
("addr", address.clone()),
("signature", signature_hex_str),
("nonceHash", login_resp.nonce_hash),
])
.send()
.await
{
Ok(response) => {
info!("Received the verify response from ETHGas.");
response
},
Err(e) => {
error!("Failed to verify signature: {}", e);
return is_logged_in;
}
};
if verify_response.status().is_success() {
let (refresh_token, refresh_token_exp) =
self.extract_refresh_token(&verify_response);
trace!("refresh token: {:?}", refresh_token);
if let ApiData::Verify(verify_resp) = verify_response
.json::<ApiResponse>()
.await
.expect("Failed to decode verification response")
.data
{
let access_token = verify_resp.access_token.token;
let access_token_exp = verify_resp.access_token.data.payload.exp;
trace!("JWT access token: {:?}", access_token);
trace!("Expired at: {:?}", access_token_exp);
let mut access_token_writer = self.state.access_token.write().await;
*access_token_writer = Some(access_token);
self.refresh_token = refresh_token;
self.access_token_exp = Some(access_token_exp);
self.refresh_token_exp = refresh_token_exp;
is_logged_in = true;
info!("ETHGas login successful.");
}
} else {
error!(
"Failed to verify ETHGas login signature, Err: {:?}",
verify_response.text().await
);
}
}
}
}
is_logged_in
}
fn extract_refresh_token(&self, response: &Response) -> (Option<String>, Option<i64>) {
// Extract the cookies from the response
let cookies: HashMap<String, String> = response
.cookies()
.map(|cookie| (cookie.name().to_string(), cookie.value().to_string()))
.collect();
// 7 days
let refresh_token_exp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Failed to get current time")
.as_secs() as i64
+ Duration::from_secs(7 * 24 * 60 * 60).as_secs() as i64;
// Look for the x_auth_refresh_token cookie and return its value
(
cookies.get("x_auth_refresh_token").cloned(),
Some(refresh_token_exp),
)
}
fn is_token_expired(&self, target_ts: i64) -> bool {
// Get the current time in UTC
let current_time = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Failed to get current time")
.as_secs() as i64;
current_time >= target_ts
}
pub async fn refresh_access(&mut self) {
if self.refresh_token_exp.is_some() {
let refresh_expiry: i64 = self.refresh_token_exp.unwrap() - (24 * 60 * 60);
if self.is_token_expired(refresh_expiry) {
let logged_in = self.login().await;
if !logged_in {
error!("Failed to refresh access token due to failed login.");
}
} else {
let start = Instant::now();
debug!("refreshing access token...");
self.refresh_access_token().await;
let elapsed = start.elapsed();
debug!("refresh access token elapsed: {:?}", elapsed);
}
} else {
error!(
"Failed to refresh access token due to missing refresh token expiry, Err: {:?}",
self.refresh_token_exp
);
}
}
async fn refresh_access_token(&mut self) {
if self.access_token_exp.is_some() {
// Get the target timestamp (access token expiration - 20 minutes)
let access_expiry: i64 = self.access_token_exp.unwrap() - (20 * 60);
if !self.is_token_expired(access_expiry) {
return;
}
}
let refresh_headers = self.get_headers().await;
let refresh_url = format!("{}/v1/user/login/refresh", self.api_url);
let refresh_token = self.refresh_token.clone().unwrap();
let refresh_response = self
.client
.post(&refresh_url)
.headers(refresh_headers)
.form(&[("refreshToken", refresh_token.as_str())])
.send()
.await
.expect("Failed to refresh access token");
if refresh_response.status().is_success() {
if let ApiData::Verify(refresh_resp) = refresh_response
.json::<ApiResponse>()
.await
.expect("Failed to decode refresh response")
.data
{
debug!(
"new JWT access token: {:?}",
refresh_resp.access_token.token
);
let mut access_token_writer = self.state.access_token.write().await;
*access_token_writer = Some(refresh_resp.access_token.token);
self.access_token_exp = Some(refresh_resp.access_token.data.payload.exp);
}
} else {
error!(
"Failed to refresh access token, Err: {:?}",
refresh_response.text().await
);
}
}
}
Repository: ethgas-rs
Overview
The Go connector provides a robust, concurrent way to interact with the ETHGas platform. The connector includes a REST client for making HTTP requests with built-in retry logic and connection pooling, and a WebSocket client for real-time data streaming with automatic reconnection handling.
For private endpoint access, the connector implements the complete authentication workflow including EIP-712 message signing, JWT token management, and automatic token refresh with goroutine-based background processing.
Example Usage
// ApiClient represents the client for interacting with the API
type ApiClient struct {
APIURL string
ChainID string
Client *http.Client
AccessToken string
RefreshToken string // Keeping this as a field for storing the refresh token
}
// LoginResponse represents the login response structure
type LoginResponse struct {
Status string `json:"status"`
EIP712Message string `json:"eip712Message"`
NonceHash string `json:"nonceHash"`
}
// VerifyResponse represents the verification response structure
type VerifyResponse struct {
User User `json:"user"`
AccessToken AccessToken `json:"accessToken"`
}
// AccessToken represents the access token structure
type AccessToken struct {
Token string `json:"token"`
}
// ApiResponse represents the standard API response structure
type ApiResponse struct {
Success bool `json:"success"`
Data json.RawMessage `json:"data"`
}
func (c *ApiClient) Login(privateKey string) (string, string, error) {
// Check cache first
loginCache.mutex.RLock()
if loginCache.AccessToken != "" && loginCache.RefreshToken != "" {
// If cache is less than 23 hours old, use it
if time.Since(loginCache.LastLogin) < 23*time.Hour {
accessToken := loginCache.AccessToken
refreshToken := loginCache.RefreshToken
loginCache.mutex.RUnlock()
return accessToken, refreshToken, nil
}
}
loginCache.mutex.RUnlock()
// If cache miss or expired, try to login with retries
var lastErr error
for i := 0; i < maxRetries; i++ {
accessToken, refreshToken, err := c.tryLogin(privateKey)
if err == nil {
// Update cache on success
loginCache.mutex.Lock()
loginCache.AccessToken = accessToken
loginCache.RefreshToken = refreshToken
loginCache.LastLogin = time.Now()
loginCache.mutex.Unlock()
return accessToken, refreshToken, nil
}
lastErr = err
log.Printf("Login attempt %d failed: %v, retrying in %v...", i+1, err, retryDelay)
time.Sleep(retryDelay)
}
return "", "", fmt.Errorf("all login attempts failed: %v", lastErr)
}
// Split out the actual login attempt into a separate method
func (c *ApiClient) tryLogin(privateKey string) (string, string, error) {
privateKeyBytes, err := hex.DecodeString(privateKey)
if err != nil {
return "", "", fmt.Errorf("failed to decode private key: %w", err)
}
privateKeyECDSA, err := crypto.ToECDSA(privateKeyBytes)
if err != nil {
return "", "", fmt.Errorf("failed to create ECDSA private key: %w", err)
}
address := crypto.PubkeyToAddress(privateKeyECDSA.PublicKey).Hex()
loginURL := fmt.Sprintf("%s/v1/user/login", c.APIURL)
formData := url.Values{}
formData.Set("addr", address)
formData.Set("chainId", c.ChainID)
req, err := http.NewRequest("POST", loginURL, strings.NewReader(formData.Encode()))
if err != nil {
return "", "", fmt.Errorf("failed to create login request: %w", err)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("User-Agent", "Relay/1.0")
resp, err := c.Client.Do(req)
if err != nil {
return "", "", fmt.Errorf("failed to send login request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := ioutil.ReadAll(resp.Body)
return "", "", fmt.Errorf("login failed with status %d: %s", resp.StatusCode, string(body))
}
var apiResponse ApiResponse
if err := json.NewDecoder(resp.Body).Decode(&apiResponse); err != nil {
return "", "", fmt.Errorf("failed to parse login response: %w", err)
}
if !apiResponse.Success {
fmt.Println("Failed response:", apiResponse)
return "", "", fmt.Errorf("login failed: response indicates failure")
}
var loginData LoginResponse
if err := json.Unmarshal(apiResponse.Data, &loginData); err != nil {
return "", "", fmt.Errorf("failed to parse login data: %w", err)
}
fmt.Println("EIP712Message:", loginData.EIP712Message)
fmt.Println("NonceHash:", loginData.NonceHash)
// Parse the EIP712Message JSON string into apitypes.TypedData
var typedData apitypes.TypedData
if err := json.Unmarshal([]byte(loginData.EIP712Message), &typedData); err != nil {
return "", "", fmt.Errorf("failed to unmarshal EIP712Message: %w", err)
}
// signature, err := SignEIP712Message(privateKeyECDSA, loginData.EIP712Message)
signature, err := eip712.SignTypedData(typedData, privateKeyECDSA)
var signatureHash = hex.EncodeToString(signature)
if err != nil {
return "", "", fmt.Errorf("failed to sign EIP712 message: %w", err)
}
fmt.Println("Signature:", signatureHash)
// verifyURL := fmt.Sprintf("%s/v1/user/login/verify", c.APIURL)
verifyURL := fmt.Sprintf("%s/v1/user/login/verify", c.APIURL)
verifyFormData := url.Values{}
verifyFormData.Set("addr", address)
verifyFormData.Set("signature", signatureHash)
verifyFormData.Set("nonceHash", loginData.NonceHash)
// Create a new request with User-Agent
verifyReq, err := http.NewRequest("POST", verifyURL, strings.NewReader(verifyFormData.Encode()))
if err != nil {
return "", "", fmt.Errorf("failed to create verification request: %w", err)
}
verifyReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
verifyReq.Header.Set("User-Agent", "Relay/1.0")
verifyResp, err := c.Client.Do(verifyReq)
if err != nil {
return "", "", fmt.Errorf("failed to send verification request: %w", err)
}
defer verifyResp.Body.Close()
if verifyResp.StatusCode != http.StatusOK {
body, _ := ioutil.ReadAll(verifyResp.Body)
return "", "", fmt.Errorf("verification failed with status %d: %s", verifyResp.StatusCode, string(body))
}
var verifyApiResponse ApiResponse
if err := json.NewDecoder(verifyResp.Body).Decode(&verifyApiResponse); err != nil {
return "", "", fmt.Errorf("failed to parse verification response: %w", err)
}
if !verifyApiResponse.Success {
return "", "", fmt.Errorf("verification failed: response indicates failure")
}
var verifyData VerifyResponse
if err := json.Unmarshal(verifyApiResponse.Data, &verifyData); err != nil {
return "", "", fmt.Errorf("failed to parse verify data: %w", err)
}
c.AccessToken = verifyData.AccessToken.Token
c.RefreshToken = c.extractRefreshToken(verifyResp)
return c.AccessToken, c.RefreshToken, nil
}
// Update the RefreshAccessToken method to include retries
func (c *ApiClient) RefreshAccessToken() error {
var lastErr error
for i := 0; i < maxRetries; i++ {
err := c.tryRefreshAccessToken()
if err == nil {
return nil
}
lastErr = err
log.Printf("Token refresh attempt %d failed: %v, retrying in %v...", i+1, err, retryDelay)
time.Sleep(retryDelay)
}
return fmt.Errorf("all token refresh attempts failed: %v", lastErr)
}
// RefreshAccessToken refreshes the access token using the refresh token
// Split out the actual refresh attempt into a separate method
func (c *ApiClient) tryRefreshAccessToken() error {
refreshURL := fmt.Sprintf("%s/v1/user/login/refresh", c.APIURL)
formData := url.Values{}
formData.Set("refreshToken", c.RefreshToken)
req, err := http.NewRequest("POST", refreshURL, strings.NewReader(formData.Encode()))
if err != nil {
return fmt.Errorf("failed to create refresh token request: %w", err)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("User-Agent", "Relay/1.0")
resp, err := c.Client.Do(req)
if err != nil {
return fmt.Errorf("failed to send refresh token request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := ioutil.ReadAll(resp.Body)
return fmt.Errorf("refresh token failed with status %d: %s", resp.StatusCode, string(body))
}
// Parse the refresh response
var apiResponse ApiResponse
if err := json.NewDecoder(resp.Body).Decode(&apiResponse); err != nil {
return fmt.Errorf("failed to parse refresh token response: %w", err)
}
if !apiResponse.Success {
return fmt.Errorf("refresh token failed: response indicates failure")
}
// Extract the verify data
var verifyData VerifyResponse
if err := json.Unmarshal(apiResponse.Data, &verifyData); err != nil {
return fmt.Errorf("failed to parse verify data: %w", err)
}
// Save new access token
c.AccessToken = verifyData.AccessToken.Token
return nil
}
func (c *ApiClient) extractRefreshToken(resp *http.Response) string {
for _, cookie := range resp.Cookies() {
if cookie.Name == "x_auth_refresh_token" {
return cookie.Value
}
}
return ""
}
func InitLoginAndStartTokenRefresh() {
accessToken, refreshToken, err := client.Login(exchangeLoginPrivateKey)
if err != nil || accessToken == "" || refreshToken == "" {
log.Printf("Failed to login during initialization: %v", err)
return
}
go client.startTokenRefreshLoop()
go client.startDailyLoginLoop(exchangeLoginPrivateKey)
}
func (c *ApiClient) startTokenRefreshLoop() {
log.Println("Starting access token refresh loop...")
ticker := time.NewTicker(30 * time.Minute)
defer ticker.Stop()
for range ticker.C {
err := c.RefreshAccessToken()
if err != nil {
log.Printf("Failed to refresh access token: %v", err)
}
}
}
func (c *ApiClient) startDailyLoginLoop(privateKey string) {
log.Println("Starting daily login loop...")
ticker := time.NewTicker(24 * time.Hour)
defer ticker.Stop()
for range ticker.C {
accessToken, refreshToken, err := c.Login(privateKey)
if err != nil {
log.Printf("Failed to login during daily refresh: %v", err)
continue
}
if accessToken == "" || refreshToken == "" {
log.Printf("Received empty tokens during daily refresh")
continue
}
}
}
Repository: ethgas-go
Environments
| Environment | Chain | RPC URL | Chain ID | API base URL | Websocket base URL | Collateral Contract |
|---|---|---|---|---|---|---|
| Mainnet | Ethereum Chain | https://eth.llamarpc.com | 1 | https://mainnet.app.ethgas.com/api | wss://mainnet.app.ethgas.com/ws | 0x3314Fb492a5d205A601f2A0521fAFbD039502Fc3 |
| Testnet | Hoodi Chain | https://ethereum-hoodi-rpc.publicnode.com | 560048 | https://hoodi.app.ethgas.com/api | wss://hoodi.app.ethgas.com/ws | 0x104Ef4192a97E0A93aBe8893c8A2d2484DFCBAF1 |
- Chain IDs are shown in decimal (e.g. Hoodi testnet is
560048, hex0x88BB0). - User can get a list of mainnet rpc urls from https://chainlist.org/chain/1
Configuration
# Mainnet
ETHGAS_API_URL=https://mainnet.app.ethgas.com/api
ETHGAS_WS_URL=wss://mainnet.app.ethgas.com/ws
# Testnet (Hoodi)
ETHGAS_API_URL=https://hoodi.app.ethgas.com/api
ETHGAS_WS_URL=wss://hoodi.app.ethgas.com/ws
Endpoint paths in this documentation (e.g. /v1/user/login, /v1/builder/register) are relative to ETHGAS_API_URL. The full URL is $ETHGAS_API_URL + path (e.g. https://mainnet.app.ethgas.com/api/v1/user/login).
REST API
Response Structure
Every API response contains a success flag and a response body.
| Name | Type | Description |
|---|---|---|
| success | boolean | API call is successful or unsuccessful |
| data | object | Response body |
Authentication Flow
ETHGas uses JWT Bearer token authentication as mentioned above. Here's the complete flow:
Login
First, authenticate with your credentials to get the EIP712 message for signing.
- HTTP
- Python
Example Code:
curl -X POST "$ETHGAS_API_URL/v1/user/login?addr=0x8F02425B5f3c522b7EF8EA124162645F0397c478&name=username"
Example Response:
{
"success": true,
"data": {
"nonceHash": "0x...",
"eip712Message": "{...}"
}
}
import os
import requests
ETHGAS_API_URL = os.environ.get("ETHGAS_API_URL", "https://mainnet.app.ethgas.com/api")
url = f"{ETHGAS_API_URL}/v1/user/login"
payload = {
'addr': '0x5eF1B2c02f5E39C0fF667611C5d7EfFb0E7df305',
'name': 'username'
}
headers = {
'Content-Type': 'application/json'
}
response = requests.post(url, headers=headers, params=payload)
print(response.text)
Request Parameters:
| Parameter | Required | Type | Description |
|---|---|---|---|
| addr | YES | string | User's EOA account (account) address |
| name | NO | string | Display name |
Response Body:
| Name | Type | Description |
|---|---|---|
| status | string | Login status |
| eip712Message | object | EIP712 message |
| nonceHash | string | A unique four-byte nonce to identify this particular login request |
Usage
Get the response from /v1/user/login and sign the eip712Message and send the signed message through /v1/user/login/verify.
Verify Login
After receiving the EIP712 message from the login endpoint, sign it and send the signature to verify your account ownership.
- HTTP
- Python
Example Code:
curl -X POST "$ETHGAS_API_URL/v1/user/login/verify?addr=0xe61f536f031f77C854b21652aB0F4fBe7CF3196F&nonceHash=517d9272&signature=0xc046037ec795f4cfe7aca33a0c283c0152bae91008b3e14b84be50f91f0e2db714054dee85b840e3edf0e26480231a684447f48337de64ea6697a3552aa9351a1b"
Response:
{
"success": true,
"data": {
"user": {
"userId": 78,
"address": "0xe61f536f031f77c854b21652ab0f4fbe7cf3196f",
"status": 1,
"userType": 1,
"userClass": 1,
"accounts": [
{
"accountId": 242,
"userId": 78,
"type": 1,
"name": "Current",
"status": 1,
"updateDate": 1698127521000
},
{
"accountId": 243,
"userId": 78,
"type": 2,
"name": "Preconf",
"status": 1,
"updateDate": 1698127521000
}
]
},
"accessToken": {
"data": {
"header": {
"alg": "ES256",
"typ": "JWT"
},
"payload": {
"user": {
"userId": 78,
"address": "0xe61f536f031f77c854b21652ab0f4fbe7cf3196f",
"roles": [
"ROLE_USER"
]
},
"access_type": "access_token",
"iat": 1698633225,
"exp": 1698636825
}
},
"token": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7InVzZXJJZCI6NzgsImFkZHJlc3MiOiIweGU2MWY1MzZmMDMxZjc3Yzg1NGIyMTY1MmFiMGY0ZmJlN2NmMzE5NmYiLCJyb2xlcyI6WyJST0xFX1VTRVIiXX0sImFjY2Vzc190eXBlIjoiYWNjZXNzX3Rva2VuIiwiaWF0IjoxNjk4NjMzMjI1LCJleHAiOjE2OTg2MzY4MjV9.E3aIKqqFsHVBYedAuqn6Jw6bymsWy6RQ6gf_lDXnYNorjngA05uFLaTM0A2ZrN4kJ8nTXEjlrdhLU8crisJcdA"
}
}
}
Example Code:
import os
import requests
ETHGAS_API_URL = os.environ.get("ETHGAS_API_URL", "https://mainnet.app.ethgas.com/api")
url = f"{ETHGAS_API_URL}/v1/user/login/verify"
payload = {
"addr": "0xe61f536f031f77C854b21652aB0F4fBe7CF3196F",
"nonceHash": "517d9272",
"signature": "0xc046037ec795f4cfe7aca33a0c283c0152bae91008b3e14b84be50f91f0e2db714054dee85b840e3edf0e26480231a684447f48337de64ea6697a3552aa9351a1b"
}
headers = {
'Content-Type': 'application/json'
}
response = requests.post(url, headers=headers, params=payload)
print(response.text)
Example Response:
{
"success": true,
"data": {
"user": {
"userId": 78,
"address": "0xe61f536f031f77c854b21652ab0f4fbe7cf3196f",
"status": 1,
"userType": 1,
"userClass": 1,
"accounts": [
{
"accountId": 242,
"userId": 78,
"type": 1,
"name": "Current",
"status": 1,
"updateDate": 1698127521000
},
{
"accountId": 243,
"userId": 78,
"type": 2,
"name": "Preconf",
"status": 1,
"updateDate": 1698127521000
}
]
},
"accessToken": {
"data": {
"header": {
"alg": "ES256",
"typ": "JWT"
},
"payload": {
"user": {
"userId": 78,
"address": "0xe61f536f031f77c854b21652ab0f4fbe7cf3196f",
"roles": [
"ROLE_USER"
]
},
"access_type": "access_token",
"iat": 1698633225,
"exp": 1698636825
}
},
"token": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7InVzZXJJZCI6NzgsImFkZHJlc3MiOiIweGU2MWY1MzZmMDMxZjc3Yzg1NGIyMTY1MmFiMGY0ZmJlN2NmMzE5NmYiLCJyb2xlcyI6WyJST0xFX1VTRVIiXX0sImFjY2Vzc190eXBlIjoiYWNjZXNzX3Rva2VuIiwiaWF0IjoxNjk4NjMzMjI1LCJleHAiOjE2OTg2MzY4MjV9.E3aIKqqFsHVBYedAuqn6Jw6bymsWy6RQ6gf_lDXnYNorjngA05uFLaTM0A2ZrN4kJ8nTXEjlrdhLU8crisJcdA"
}
}
}
Request Parameters:
| Parameter | Required | Type | Description |
|---|---|---|---|
| addr | YES | string | User's EOA account address |
| nonceHash | YES | string | The nonce hash received from the login endpoint |
| signature | YES | string | The signed EIP712 message signature |
Response Body:
| Name | Type | Description |
|---|---|---|
| accessToken | object | JWT access token with token and expiresAt fields |
| refreshToken | string | Token used to refresh the access token |
| user | object | User account information |
Usage
Sign the EIP712 message received from /v1/user/login and send the signature along with the nonceHash to /v1/user/login/verify to complete authentication and receive your access token.
Token Refresh
- HTTP
- Python
curl -X POST "$ETHGAS_API_URL/v1/user/login/refresh" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <current_access_token>" \
-d '{"refreshToken": "your_refresh_token"}'
import os
import requests
ETHGAS_API_URL = os.environ.get("ETHGAS_API_URL", "https://mainnet.app.ethgas.com/api")
body = {'refreshToken': refresh_token}
response = requests.post(
f"{ETHGAS_API_URL}/v1/user/login/refresh",
cookies=response_cookies,
data=body
)
access_token = response.json()['data']['accessToken']['token']
headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + access_token
}
response_cookies = response.cookies
Logout
- HTTP
- Python
POST /v1/user/logout
Authorization: Bearer <access_token>
Response:
{
"success": true,
"data": {
"message": "Logged out successfully"
}
}
headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + access_token
}
response = requests.post(domain + '/v1/user/logout', headers=headers)
print(response.json())
WebSocket Connection
For real-time data, establish a WebSocket connection:
- Testnet
- Mainnet
WebSocket URL: wss://hoodi.app.ethgas.com/ws
Configuration:
ETHGAS_WS_URL=wss://hoodi.app.ethgas.com/ws
Environment: Testnet (Hoodi)
Purpose: Development, testing, and integration
Data: Simulated market data and test transactions
WebSocket URL: wss://mainnet.app.ethgas.com/ws
Configuration:
ETHGAS_WS_URL=wss://mainnet.app.ethgas.com/ws
Environment: Mainnet
Purpose: Production trading and real transactions
Data: Live market data and real transactions
WebSocket Authentication
Authenticate your WebSocket connection:
{
"cmd": "login",
"args": {
"accessToken": "YOUR_ACCESS_TOKEN"
}
}
WebSocket Message Structure
All WebSocket messages follow this format:
{
"cmd": "command_name",
"args": {
"parameter1": "value1",
"parameter2": "value2"
}
}
Common WebSocket Commands
| Command | Description |
|---|---|
login | Authenticate the connection |
subscribe | Subscribe to real-time updates |
unsubscribe | Unsubscribe from updates |
query | Request specific data |
Infrastructure
Relay Endpoints
Official relay endpoints by region:
| Region | Environment | Endpoint |
|---|---|---|
| Tokyo | Mainnet | https://0x88ef3061f598101ca713d556cf757763d9be93d33c3092d3ab6334a36855b6b4a4020528dd533a62d25ea6648251e62e@ap-relay.ethgas.com |
| Tokyo | Hoodi | https://0xb20c3fe59db9c3655088839ef3d972878d182eb745afd8abb1dd2abf6c14f93cd5934ed4446a5fe1ba039e2bc0cf1011@hoodi-relay.ethgas.com |
| Frankfurt | Mainnet | https://0x88ef3061f598101ca713d556cf757763d9be93d33c3092d3ab6334a36855b6b4a4020528dd533a62d25ea6648251e62e@eu-relay.ethgas.com |
| Virginia | Mainnet | https://0x88ef3061f598101ca713d556cf757763d9be93d33c3092d3ab6334a36855b6b4a4020528dd533a62d25ea6648251e62e@us-relay.ethgas.com |
Prepend the key as shown in the endpoint URLs for authentication.
Error Handling
ETHGas uses standard HTTP status codes and custom error codes. See the Error Codes section for details.
Next Steps
- REST API
- WebSocket Guide — Real-time data streaming
- Authentication API — Detailed auth reference
- Changelog — Release history