npm install
cp .env.example .env
npm run devdocker build -t my-app .
docker run --rm --env-file .env my-appMNEMONIC: 12/24-word BIP39 phrase used to derive the signer (m/44'/60'/0'/0/0).AUTHORIZED_ADDRESSES: Comma-separated list of Ethereum addresses allowed to sign requests (e.g.,0x1234...,0x5678...).APP_PORT(optional): server port, defaults to8080.APP_HOST(optional): server host, defaults to127.0.0.1.TIMESTAMP_WINDOW(optional): Allowed timestamp difference in seconds, defaults to300(5 minutes).NONCE_TTL(optional): Nonce time-to-live in seconds, defaults to600(10 minutes).
Returns the EOA address derived from the mnemonic.
Response:
{
"address": "0x..."
}Signs a hash with the mnemonic. Protected by ECDSA signature authentication.
Authentication: Requests must include the following headers:
X-Client-Id: Ethereum address of the signerX-Timestamp: Unix timestamp (seconds)X-Nonce: Unique random string (UUID or random hex)X-Signature: ECDSA signature of${timestamp}|${nonce}|0x${bodyHash}
Request Body:
{
"hash": "0x..."
}Response:
{
"signature": "0x..."
}Error Responses:
400: Missing or invalid request body/headers401: Authentication failed (invalid signature, expired timestamp, reused nonce)403: Client address not authorized500: Server error
The service uses ECDSA signature authentication for the /sign endpoint. Clients must:
- Generate a random nonce (UUID or random hex string)
- Get current Unix timestamp
- Hash the request body with SHA256:
bodyHash = SHA256(JSON.stringify(body)) - Construct message:
${timestamp}|${nonce}|0x${bodyHash} - Sign the message with their private key using EIP-191 personal sign format
- Send request with all required headers
Client Implementation Example:
import { privateKeyToAccount } from 'viem/accounts';
import { createHash, randomBytes } from 'crypto';
const account = privateKeyToAccount('0x...'); // Client's private key
const clientId = account.address;
// Generate nonce
const nonce = '0x' + randomBytes(32).toString('hex');
// Or: const nonce = randomUUID();
// Get timestamp
const timestamp = Math.floor(Date.now() / 1000).toString();
// Prepare request body
const body = { hash: '0x...' };
const bodyString = JSON.stringify(body);
// Hash the body
const bodyHash = createHash('sha256').update(bodyString).digest('hex');
// Construct message to sign
const message = `${timestamp}|${nonce}|0x${bodyHash}`;
// Sign the message
const signature = await account.signMessage({ message });
// Send request
const response = await fetch('http://localhost:8080/sign', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Client-Id': clientId,
'X-Timestamp': timestamp,
'X-Nonce': nonce,
'X-Signature': signature,
},
body: bodyString,
});Security Features:
- Replay Attack Prevention: Nonces are tracked and can only be used once
- Timestamp Validation: Requests must be within the configured time window
- Request Integrity: Body hash is included in the signature, preventing tampering
- Address Whitelist: Only addresses in
AUTHORIZED_ADDRESSEScan sign requests
Before deploying, you'll need:
- Docker - To package and publish your application image
- Download Docker
- You'll also need to
docker loginto push images to your registry
- ETH - To pay for deployment transactions
- For Sepolia testnet: Google Cloud Faucet or Alchemy Faucet
# Store your private key (generate new or use existing)
eigenx auth generate --store
# OR: eigenx auth login (if you have an existing key)
eigenx app deploy username/image-nameThe CLI will automatically detect the Dockerfile and build your app before deploying.
eigenx app list # List all apps
eigenx app info [app-name] # Get app details
eigenx app logs [app-name] # View logs
eigenx app start [app-name] # Start stopped app
eigenx app stop [app-name] # Stop running app
eigenx app terminate [app-name] # Terminate app
eigenx app upgrade [app-name] [image] # Update deployment
eigenx app configure tls # Configure TLSeigenx app profile set [app-id] # Set app name, website, description, social links, and iconThis project includes optional automatic TLS certificate management using Caddy. The Caddyfile is not required - if you don't need TLS termination or prefer to handle it differently, you can simply delete the Caddyfile.
When a Caddyfile is present in your project root:
- Caddy will automatically start as a reverse proxy
- It handles TLS certificate acquisition and renewal via Let's Encrypt
- Your app runs on
APP_PORTand Caddy forwards HTTPS traffic to it - Certificates are stored persistently in the TEE's encrypted storage
Without a Caddyfile:
- Your application runs directly on the configured ports
- You can handle TLS in your application code or use an external load balancer
Before deploying with TLS:
-
Configure TLS: Run
eigenx app configure tlsto add the necessary configuration files for domain setup with private traffic termination in the TEE. -
DNS: Ensure A/AAAA record points to your instance (or reserved static IP). Note: If this is your first deployment, you will need to get your IP after deployment from the
eigenx app infocommand. -
Required configuration in
.env:DOMAIN=mydomain.com # Your domain name APP_PORT=8000 # Your app's port ACME_STAGING=true # Test with staging first to avoid rate limits ENABLE_CADDY_LOGS=true # Enable logs for debugging
-
Optional ACME configuration (all optional, with sensible defaults):
# ACME email for Let's Encrypt notifications ACME_EMAIL=admin@example.com # Certificate Authority directory URL # Default: https://acme-v02.api.letsencrypt.org/directory ACME_CA=https://acme-v02.api.letsencrypt.org/directory # ACME Challenge Type # How to prove domain ownership to Let's Encrypt # Both result in the same TLS certificate, just different validation methods: # - http-01: Uses port 80 (default) # - tls-alpn-01: Uses port 443 ACME_CHALLENGE=http-01 # Use Let's Encrypt Staging (for testing) # Set to true to use staging environment (certificates won't be trusted by browsers) # Great for testing without hitting rate limits ACME_STAGING=true # Force certificate reissue # Set to true to force a new certificate even if one exists # This will delete the existing certificate from storage and get a new one ACME_FORCE_ISSUE=true
-
Customize Caddyfile (optional):
- Edit
Caddyfileto match your application port - Modify security headers as needed
- Configure rate limiting or other middleware
- Edit
-
Enable Caddy logs to see TLS-related output:
ENABLE_CADDY_LOGS=true
-
Use Let's Encrypt staging for testing (avoids rate limits, but certificates won't be trusted by browsers):
ACME_STAGING=true
For local development without TLS, leave DOMAIN empty or set to localhost in your .env file.
To use custom certificates instead of Let's Encrypt, modify the Caddyfile:
tls /path/to/cert.pem /path/to/key.pem