Secure Ethereum wallet server with two-phase commit pattern for all fund-moving operations. Optionally integrates with Polymarket for prediction market trading on Polygon.
- Two-Phase Commit: All transactions require explicit prepare → confirm flow
- API Key Authentication: SHA-256 hashed keys with read/write permissions
- Rate Limiting: Per-key limits via Redis
- Transaction Simulation: All operations validated via
eth_callbefore storing - Private Key Isolation: Loaded once, never logged or exposed
- Contract Allowlist: Enforced for vault/DEX interactions
- ERC-20 Transfers & Approvals: Send tokens, approve spenders
- DeFi: Uniswap V3 swaps, ERC-4626 vault deposits/withdrawals
Enabled when POLYGON_RPC_URL is set. All endpoints are under /polymarket/.
- CLOB Trading: Limit orders, market orders, cancel orders via Polymarket's off-chain CLOB
- Market Discovery: Browse tags, events, and markets (Tags → Events → Markets hierarchy), view orderbooks, get midpoint prices (via Gamma + CLOB APIs)
- Close Position: One-call convenience endpoint to close a position (auto-resolves size, token_id, best bid)
- Setup Management: Batch-prepare all 7 Polygon approval transactions, derive CLOB credentials
- CTF Inventory: Split USDC.e into YES/NO tokens, merge back, redeem resolved markets
- Portfolio: View positions with token IDs, open orders, trade history, USDC.e balance on Polygon
- Python 3.11+
- Redis server running locally or remotely
- Ethereum RPC endpoint (Alchemy, Infura, or your own node)
- A funded Ethereum wallet (for testing)
- (Optional) Polygon RPC endpoint for Polymarket features
# Clone and enter directory
cd moltwallet
# Create virtual environment
python3 -m venv venv
source venv/bin/activate
# Install dependencies
pip install -e ".[dev]"python scripts/generate_api_key.py --permission writeThis outputs:
- API Key: Give this to clients (store securely!)
- Hash:Permission: Add this to your
API_KEYSenv var
cp .env.example .envEdit .env with your values:
# === Required (Classic Wallet) ===
# Your wallet's private key (WITH 0x prefix)
PRIVATE_KEY=0x...
# Ethereum RPC endpoint
RPC_URL=https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY
# Chain ID (1 = mainnet, 11155111 = sepolia)
CHAIN_ID=1
# Redis connection
REDIS_URL=redis://localhost:6379/0
# API keys (from generate_api_key.py output)
API_KEYS=abc123hash:write,def456hash:read_only
# Optional: Allowed contracts for DeFi operations
ALLOWED_CONTRACTS=0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45
# === Optional (Polymarket) ===
# Set POLYGON_RPC_URL to enable all /polymarket/* endpoints.
# Without it, Polymarket routes are not registered.
POLYGON_RPC_URL=https://polygon-mainnet.g.alchemy.com/v2/YOUR_KEY
# CLOB credentials (optional — can also be derived via /polymarket/setup/credentials/create)
POLY_API_KEY=
POLY_API_SECRET=
POLY_API_PASSPHRASE=
# Trading guardrails
POLY_MAX_ORDER_SIZE=1000.0
POLY_MAX_PRICE_DEVIATION=0.10# macOS
brew services start redis
# Or run directly
redis-server# Development
flask --app "app:create_app()" run --debug --port 8000
# Production
gunicorn --config gunicorn.conf.py "app:create_app()"All endpoints (except /health) require the X-API-Key header:
curl -H "X-API-Key: your_api_key_here" http://localhost:8000/balance/ethBoth classic wallet and Polymarket on-chain operations use the same prepare → confirm pattern.
Step 1: Prepare - Validate and simulate the transaction
curl -X POST http://localhost:8000/transactions/eth/prepare \
-H "X-API-Key: YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"to_address": "0x742d35Cc6634C0532925a3b844Bc9e7595f5b1E2",
"amount_eth": "0.01"
}'Response:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"type": "eth_transfer",
"status": "pending",
"expires_at": "2024-01-01T12:05:00Z",
"summary": {
"action": "ETH Transfer",
"to": "0x742d35Cc6634C0532925a3b844Bc9e7595f5b1E2",
"amount": "0.01 ETH"
},
"simulation": {
"success": true,
"gas_used": 21000
}
}Step 2: Confirm - Sign and broadcast
curl -X POST http://localhost:8000/operations/550e8400-e29b-41d4-a716-446655440000/confirm \
-H "X-API-Key: YOUR_KEY"Response:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"status": "confirmed",
"result": {
"tx_hash": "0x...",
"status": "broadcast"
}
}| Method | Endpoint | Permission | Description |
|---|---|---|---|
| GET | /health |
None | Health check |
| GET | /balance/eth |
read | ETH balance |
| GET | /balance/token/{address} |
read | ERC-20 balance |
| GET | /balance/wallet |
read | Wallet address |
| GET | /gas |
read | Current gas prices |
| POST | /transactions/eth/prepare |
write | Prepare ETH transfer |
| POST | /transactions/token/prepare |
write | Prepare token transfer |
| POST | /transactions/vault-shares/prepare |
write | Prepare vault share transfer |
| POST | /defi/approval/prepare |
write | Prepare token approval |
| POST | /defi/swap/prepare |
write | Prepare DEX swap |
| GET | /defi/swap/quote |
read | Get swap quote |
| POST | /defi/vault/deposit/prepare |
write | Prepare vault deposit |
| POST | /defi/vault/withdraw/prepare |
write | Prepare vault withdrawal |
| GET | /defi/vault/{address}/info |
read | Vault information |
| GET | /defi/approvals/{token}/{spender} |
read | Check allowance |
| GET | /operations/pending |
read | List pending operations |
| GET | /operations/{id} |
read | Get operation details |
| POST | /operations/{id}/confirm |
write | Execute operation |
| POST | /operations/{id}/cancel |
write | Cancel operation |
| GET | /transactions/tx/{hash} |
read | Transaction status |
All Polymarket endpoints require POLYGON_RPC_URL to be set. Without it, these routes return 404.
| Method | Endpoint | Permission | Description |
|---|---|---|---|
| GET | /polymarket/setup/status |
read | Credential + RPC + allowance status |
| POST | /polymarket/setup/allowances/prepare |
write | Prepare all 7 approval txs |
| POST | /polymarket/setup/credentials/create |
write | Derive CLOB API credentials |
Polymarket data is organized in a hierarchy: Tags → Events → Markets.
- Tags categorize topics (e.g. "Crypto", "Sports", "Politics"). Each has a numeric
tag_idused for filtering. - Events are questions or topics that group related markets (e.g. "Where will Barron Trump attend College?").
- Markets are individual tradeable outcomes within an event (e.g. "Will Barron attend Georgetown?", "Will Barron attend NYU?"). Each market has a
conditionIdand YES/NO token IDs used for trading.
The typical discovery flow is: browse tags → filter events by tag → pick an event → pick a market → get token IDs → trade.
| Method | Endpoint | Permission | Description |
|---|---|---|---|
| GET | /polymarket/tags |
read | List all tags with IDs for filtering |
| GET | /polymarket/events |
read | List events with nested markets (?tag_id=, ?limit=, ?offset=, ?active=, ?closed=) |
| GET | /polymarket/events/{slug_or_id} |
read | Single event by slug or numeric ID |
| GET | /polymarket/markets |
read | List individual markets (?tag_id=, ?limit=, ?offset=, ?active=, ?closed=) |
| GET | /polymarket/markets/{condition_id_or_slug} |
read | Market details (accepts 0x... conditionId or slug) |
| GET | /polymarket/markets/{condition_id}/book |
read | Orderbook (?token_id= required) |
| GET | /polymarket/markets/{condition_id}/price |
read | Midpoint price as float (?token_id= required) |
| Method | Endpoint | Permission | Description |
|---|---|---|---|
| POST | /polymarket/orders/limit/prepare |
write | Prepare limit order |
| POST | /polymarket/orders/market/prepare |
write | Prepare market order |
| POST | /polymarket/orders/cancel/prepare |
write | Prepare cancel order(s) |
| POST | /polymarket/orders/cancel-all/prepare |
write | Prepare cancel-all |
| POST | /polymarket/positions/close/prepare |
write | Close a position (auto-resolves size, token_id, best bid) |
| Method | Endpoint | Permission | Description |
|---|---|---|---|
| POST | /polymarket/ctf/split/prepare |
write | Split USDC.e → YES + NO tokens |
| POST | /polymarket/ctf/merge/prepare |
write | Merge YES + NO → USDC.e |
| POST | /polymarket/ctf/redeem/prepare |
write | Redeem resolved market tokens |
| Method | Endpoint | Permission | Description |
|---|---|---|---|
| GET | /polymarket/portfolio |
read | Positions with token IDs from Data API |
| GET | /polymarket/orders/open |
read | Open orders from CLOB |
| GET | /polymarket/trades |
read | Trade history from CLOB |
| GET | /polymarket/balance |
read | USDC.e balance on Polygon |
# 1. Check setup status
curl -H "X-API-Key: YOUR_KEY" http://localhost:8000/polymarket/setup/status
# 2. Derive CLOB credentials (one-time)
curl -X POST http://localhost:8000/polymarket/setup/credentials/create \
-H "X-API-Key: YOUR_KEY"
# 3. Prepare all approval transactions (one-time)
curl -X POST http://localhost:8000/polymarket/setup/allowances/prepare \
-H "X-API-Key: YOUR_KEY"
# → Returns 7 operation IDs; confirm each via /operations/{id}/confirm
# 4. Discover tags for filtering
curl -H "X-API-Key: YOUR_KEY" http://localhost:8000/polymarket/tags
# 5. Browse events by tag (grouped markets by topic)
curl -H "X-API-Key: YOUR_KEY" \
"http://localhost:8000/polymarket/events?tag_id=1013&closed=false&limit=5"
# 6. Get a single event by slug
curl -H "X-API-Key: YOUR_KEY" \
"http://localhost:8000/polymarket/events/some-event-slug"
# 7. Get price (returns a plain float)
curl -H "X-API-Key: YOUR_KEY" \
"http://localhost:8000/polymarket/markets/CONDITION_ID/price?token_id=TOKEN_ID"
# 8. Place a limit order
curl -X POST http://localhost:8000/polymarket/orders/limit/prepare \
-H "X-API-Key: YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"condition_id": "0xabc...",
"token_id": "12345...",
"side": "BUY",
"price": 0.55,
"size": 100
}'
# → Returns operation ID
# 9. Confirm the order
curl -X POST http://localhost:8000/operations/OPERATION_ID/confirm \
-H "X-API-Key: YOUR_KEY"
# → Order posted to CLOB
# 10. Check portfolio (positions include token_id)
curl -H "X-API-Key: YOUR_KEY" http://localhost:8000/polymarket/portfolio
# 11. Close a position (auto-resolves size, token_id, and best bid)
curl -X POST http://localhost:8000/polymarket/positions/close/prepare \
-H "X-API-Key: YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"condition_id": "0xabc...", "outcome": "Yes"}'
# → Returns operation ID for a SELL limit order; confirm via /operations/{id}/confirm-
Get Sepolia ETH from a faucet:
-
Configure for Sepolia:
RPC_URL=https://eth-sepolia.g.alchemy.com/v2/YOUR_KEY CHAIN_ID=11155111
WARNING: Use a dedicated wallet with limited funds for testing.
- Create a new wallet (e.g., via MetaMask)
- Fund it with a small amount of ETH
- Export the private key
- Configure
.envwith mainnet settings
# Check health
curl http://localhost:8000/health
# Get wallet address
curl -H "X-API-Key: YOUR_KEY" http://localhost:8000/balance/wallet
# Check ETH balance
curl -H "X-API-Key: YOUR_KEY" http://localhost:8000/balance/eth
# Check gas prices
curl -H "X-API-Key: YOUR_KEY" http://localhost:8000/gas
# Prepare a small ETH transfer
curl -X POST http://localhost:8000/transactions/eth/prepare \
-H "X-API-Key: YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"to_address": "0xYOUR_OTHER_ADDRESS",
"amount_eth": "0.001"
}'
# List pending operations
curl -H "X-API-Key: YOUR_KEY" http://localhost:8000/operations/pending
# Confirm an operation (replace with actual ID)
curl -X POST http://localhost:8000/operations/OPERATION_ID/confirm \
-H "X-API-Key: YOUR_KEY"
# Or cancel it
curl -X POST http://localhost:8000/operations/OPERATION_ID/cancel \
-H "X-API-Key: YOUR_KEY"moltwallet/
├── app/
│ ├── __init__.py # Flask app factory
│ ├── config.py # Pydantic settings
│ ├── extensions.py # Redis, rate limiter
│ ├── api/
│ │ ├── middleware/ # Auth, rate limiting, errors
│ │ ├── formatters/ # Response formatters (Polymarket)
│ │ ├── routes/ # API endpoints
│ │ │ ├── health.py # Health check
│ │ │ ├── balance.py # Balance queries
│ │ │ ├── gas.py # Gas price endpoint
│ │ │ ├── transactions.py # ETH/token transfer routes
│ │ │ ├── operations.py # Operation confirm/cancel/list
│ │ │ ├── defi.py # DeFi routes (swaps, vaults)
│ │ │ └── polymarket.py # Polymarket routes (conditional)
│ │ └── schemas/ # Request/response models
│ ├── services/
│ │ ├── operation_service.py # Two-phase commit + routing
│ │ ├── simulation_service.py # eth_call simulation
│ │ ├── polymarket_service.py # Polymarket trading logic
│ │ └── clob_auth.py # CLOB credential management
│ ├── storage/ # Redis operations
│ ├── web3_ops/
│ │ ├── client.py # Primary chain Web3 singleton
│ │ ├── polygon_client.py # Polygon Web3 singleton
│ │ ├── wallet.py # Private key isolation
│ │ ├── eth_ops.py # ETH transfer builder
│ │ ├── erc20_ops.py # ERC-20 transfer/approval builders
│ │ ├── dex_ops.py # Uniswap V3 swap builders
│ │ ├── erc4626_ops.py # ERC-4626 vault operations
│ │ ├── erc1155_ops.py # ERC-1155 operations (Polymarket)
│ │ ├── ctf_ops.py # CTF split/merge/redeem (Polymarket)
│ │ └── contracts/abis/ # Contract ABIs and addresses
│ └── utils/ # Helpers
├── docs/
│ └── polymarket-learnings.md # Polymarket bugs, API gotchas, patterns
├── scripts/
│ └── generate_api_key.py
├── tests/
│ ├── unit/
│ ├── integration/
│ └── e2e/
├── .env.example
├── pyproject.toml
├── requirements.txt # Dependencies for Railway deployment
├── gunicorn.conf.py
├── Procfile # Process definition for Railway
└── nixpacks.toml # Build config for Railway/Nixpacks
- Push to GitHub
- Connect repo to Railway
- Add Redis service
- Set environment variables
- Deploy
The project includes pre-configured deployment files:
nixpacks.toml- Build configurationrequirements.txt- Python dependenciesProcfile- Gunicorn process definition
Note for Polymarket: Polymarket blocks US IPs. Deploy to a non-US region if using Polymarket features.
- Never commit
.env- it contains your private key - Use a dedicated wallet - don't use your main wallet
- Start with testnets - validate everything works first
- Monitor operations - pending ops expire after 5 minutes
- Rate limits apply - 100 reads/min, 20 writes/min per key
# Unit tests (no external deps)
pytest tests/unit/ -v
# With coverage
pytest tests/unit/ --cov=app --cov-report=htmlMIT