Skip to content

Commit 614979f

Browse files
authored
feat: Replace USDC approval modes with gasless one-time max permit (#5)
1 parent fd5fd8f commit 614979f

File tree

6 files changed

+465
-1465
lines changed

6 files changed

+465
-1465
lines changed

.claude/skills/market-maker/SKILL.md

Lines changed: 28 additions & 229 deletions
Original file line numberDiff line numberDiff line change
@@ -175,51 +175,17 @@ Present the user with these trading algorithm options for prediction markets:
175175
- Best for: Markets with overconfident pricing
176176
- Risk: Medium - based on market efficiency assumptions
177177

178-
## Step 4.5: Approval Mode Selection
179-
180-
After selecting an algorithm, ask the user which USDC approval mode they prefer:
181-
182-
**Option A: Permit-Based (Recommended for beginners)**
183-
- **No native gas token required** - Orders are fully gasless
184-
- Uses EIP-2612 permit signatures attached to each order
185-
- **Trade-off**: Requires time between orders (~5-10s) for permit nonce to increment after settlement
186-
- Best for: Casual trading, low-frequency strategies, users without native gas
187-
- Reference: `examples/price_action_bot.py`
188-
189-
**Option B: Pre-Approval (Recommended for high-frequency)**
190-
- **Requires native gas token** (ETH on Base, MATIC on Polygon) for one-time USDC approval transaction
191-
- Orders submitted without permit signatures (faster)
192-
- **Trade-off**: Must send an on-chain approval transaction at startup
193-
- Best for: High-frequency trading, stress testing, rapid order placement
194-
- Reference: `examples/price_action_bot_preapproved.py`
195-
196-
**Key differences:**
197-
198-
| Feature | Permit-Based | Pre-Approval |
199-
|---------|--------------|--------------|
200-
| Native gas needed | No | Yes (for approval TX) |
201-
| Order frequency | ~5-10s between orders | No permit delays (API rate limits apply) |
202-
| Complexity | Per-order permit signing | One-time approval |
203-
| Nonce management | Must track permit nonces | None |
204-
| Best for | Beginners, gasless | High-frequency, bots |
205-
206178
## Reference Implementation
207179

208-
Two complete, production-ready implementations exist for the **Price Action Trader**:
209-
210-
**Permit-Based: `examples/price_action_bot.py`**
211-
- Uses EIP-2612 permits for gasless trading
212-
- Includes permit nonce tracking and management
213-
- Best for users who don't want to hold native gas tokens
180+
A complete, production-ready implementation exists for the **Price Action Trader**:
214181

215-
**Pre-Approval: `examples/price_action_bot_preapproved.py`**
216-
- Uses pre-approved USDC allowance
217-
- Auto-approves USDC at startup when entering new markets
182+
**`examples/price_action_bot.py`**
183+
- Signs a single gasless max USDC permit per settlement contract on first trade
184+
- All subsequent orders use the existing allowance (no per-order permit overhead)
218185
- Order sizing in USDC terms with position tracking
219-
- Best for high-frequency trading scenarios
220186

221187
When generating a bot:
222-
1. **Read the appropriate reference file** based on approval mode chosen
188+
1. **Read `examples/price_action_bot.py`** as the primary reference
223189
2. Copy the structure exactly, customizing only configuration parameters as needed
224190
3. **DO NOT** use the simplified code snippets in this skill - they are incomplete
225191

@@ -236,16 +202,13 @@ The reference implementations include critical patterns that **MUST** be preserv
236202
- Unclaimed winnings discovery from previous sessions
237203
- Rate-limited claiming (15s delay between claims)
238204
- Async HTTP client for non-blocking external API calls
239-
240-
**Additional patterns for Pre-Approval mode:**
241-
- Auto-approval when entering new markets (`ensure_settlement_approved()`)
205+
- Gasless max permit approval when entering new markets (`ensure_settlement_approved()`)
242206
- USDC-based order sizing (`calculate_shares_from_usdc()`)
243207
- Position tracking in USDC terms (`position_usdc` dict)
244-
- Allowance checking via `client.get_usdc_allowance()`
245208

246209
## Step 5: Generate the Bot Code
247210

248-
Based on the user's algorithm and approval mode choices, generate a complete bot file. The bot should:
211+
Based on the user's algorithm choice, generate a complete bot file. The bot should:
249212

250213
1. Load credentials from environment variables
251214
2. Auto-register API keys if needed
@@ -255,13 +218,10 @@ Based on the user's algorithm and approval mode choices, generate a complete bot
255218
6. Cancel orders on shutdown
256219
7. **Automatically detect new BTC markets and switch liquidity/trades to them**
257220
8. Handle market expiration gracefully with seamless transitions
258-
9. **Handle USDC authorization based on chosen mode:**
259-
- **Permit mode**: Sign USDC permits for each order (gasless)
260-
- **Pre-approval mode**: Auto-approve USDC at startup/market switch (high-frequency)
221+
9. **Handle gasless USDC approval** via one-time max permit per settlement contract
261222
10. **Track traded markets and automatically claim winnings when they resolve**
262223

263-
**For Permit mode**: Use `examples/price_action_bot.py` as reference
264-
**For Pre-approval mode**: Use `examples/price_action_bot_preapproved.py` as reference
224+
Use `examples/price_action_bot.py` as the primary reference
265225

266226
Use this template structure for all bots:
267227

@@ -356,7 +316,7 @@ class MarketMakerBot:
356316
def __init__(self, client: TurbineClient):
357317
self.client = client
358318
self.market_id: str | None = None
359-
self.settlement_address: str | None = None # For USDC permits
319+
self.settlement_address: str | None = None # For USDC approval
360320
self.contract_address: str | None = None # For claiming winnings
361321
self.strike_price: int = 0 # BTC price when market created (6 decimals) - used by Price Action Trader
362322
self.current_position = 0
@@ -491,7 +451,7 @@ async def main():
491451
print(f"Market expires at: {quick_market.end_time}")
492452

493453
# Note gasless features
494-
print("Orders will include USDC permit signatures for gasless trading")
454+
print("USDC approval: gasless one-time max permit per settlement")
495455
print("Automatic winnings claim enabled for resolved markets")
496456
print()
497457

@@ -542,43 +502,21 @@ Note: `httpx` is used by the Price Action Trader to fetch real-time BTC prices f
542502

543503
## Step 7: Explain How to Run and Deploy
544504

545-
Tell the user based on their chosen approval mode:
505+
Tell the user:
546506

547-
**For Permit-Based bots:**
548507
```
549-
Your bot is ready! To run it locally:
508+
Your bot is ready! To run it:
550509
551510
python {bot_filename}.py
552511
553512
The bot will:
554513
- Automatically register API credentials on first run (saved to .env)
555514
- Connect to the current BTC 15-minute market
515+
- Gaslessly approve USDC on first trade per settlement (one-time max permit, no gas needed)
556516
- Start trading based on your chosen algorithm
557-
- Sign USDC permits for gasless order execution (no approval TX needed)
558517
- Automatically switch to new markets when they start
559518
- Track traded markets and claim winnings when they resolve
560519
561-
Note: Orders require ~5-10 seconds between submissions for permit nonce propagation.
562-
563-
To stop the bot, press Ctrl+C.
564-
```
565-
566-
**For Pre-Approval bots:**
567-
```
568-
Your bot is ready! To run it:
569-
570-
python {bot_filename}.py
571-
572-
The bot will:
573-
- Automatically register API credentials on first run (saved to .env)
574-
- Connect to the current BTC 15-minute market
575-
- Auto-approve USDC spending (requires native gas token: ETH on Base, MATIC on Polygon)
576-
- Start trading based on your chosen algorithm (no permit nonce delays)
577-
- Automatically switch to new markets when they start (re-approves for new settlements)
578-
- Track traded markets and claim winnings when they resolve
579-
580-
Note: Ensure your wallet has native gas tokens for the initial USDC approval transaction.
581-
582520
To stop the bot, press Ctrl+C.
583521
584522
@@ -616,7 +554,7 @@ async def run(self, host: str) -> None:
616554
await stream.subscribe(current_market)
617555
print(f"Subscribed to market {current_market[:8]}...")
618556

619-
# Place initial quotes (with USDC permits)
557+
# Place initial quotes
620558
await self.place_quotes()
621559

622560
async for message in stream:
@@ -788,172 +726,35 @@ def find_edge(self, best_bid, best_ask):
788726
- When less than 60 seconds remain, the bot logs a warning
789727
- Orders are cancelled proactively to avoid stuck orders on expired markets
790728

791-
## USDC Approval Modes
792-
793-
There are two ways to authorize USDC spending for order execution:
794-
795-
### Mode A: Permit Signatures (Gasless)
729+
## USDC Approval
796730

797-
**Every order includes a USDC permit signature** for gasless execution. No on-chain approval needed.
798-
799-
The `TurbineClient` provides `sign_usdc_permit()` to create EIP-2612 permit signatures:
800-
801-
**Pros:** No native gas token needed
802-
**Cons:** Must wait ~5-10s between orders for permit nonce to propagate after settlement
803-
804-
### Mode B: Pre-Approval (High-Frequency)
805-
806-
**Approve USDC once at startup**, then submit orders without permit signatures.
731+
Bots use a **one-time gasless max permit** for USDC approval. On first trade per settlement contract, the bot signs an EIP-2612 max permit (max value, max deadline) and submits it via the relayer. No native gas is required.
807732

808733
```python
809-
# At startup or when entering a new market:
810-
APPROVAL_AMOUNT = 1_000_000_000_000 # 1 million USDC (adjust as needed)
811-
812734
def ensure_settlement_approved(self, settlement_address: str) -> None:
813-
"""Ensure USDC is approved for this settlement contract."""
735+
"""Ensure USDC is approved for this settlement contract via gasless max permit."""
814736
if settlement_address in self.approved_settlements:
815737
return
816738

817-
# Check current allowance
739+
# Check on-chain allowance
818740
current = self.client.get_usdc_allowance(spender=settlement_address)
819-
if current >= APPROVAL_AMOUNT // 2:
820-
print(f"Existing allowance sufficient: ${current / 1e6:,.2f}")
741+
if current >= MAX_APPROVAL_THRESHOLD: # half of max uint256
821742
self.approved_settlements[settlement_address] = current
822743
return
823744

824-
# Send approval transaction (requires native gas)
825-
print(f"Approving USDC for settlement {settlement_address[:16]}...")
826-
tx_hash = self.client.approve_usdc(APPROVAL_AMOUNT, spender=settlement_address)
827-
print(f"Approval TX: {tx_hash}")
828-
829-
# Wait for confirmation
830-
time.sleep(5)
831-
self.approved_settlements[settlement_address] = APPROVAL_AMOUNT
832-
```
833-
834-
**Pros:** No permit nonce delays (API rate limits still apply), no permit nonce tracking
835-
**Cons:** Requires native gas token for approval transaction
836-
837-
### Permit-Based Order Placement (Mode A)
838-
839-
```python
840-
async def place_quotes(self) -> None:
841-
"""Place bid and ask orders with USDC permit signatures."""
842-
bid_price, ask_price = self.calculate_quotes()
843-
844-
# Place bid (buy YES)
845-
bid_order = self.client.create_limit_buy(
846-
market_id=self.market_id,
847-
outcome=Outcome.YES,
848-
price=bid_price,
849-
size=ORDER_SIZE,
850-
expiration=int(time.time()) + QUOTE_REFRESH_SECONDS + 60,
851-
settlement_address=self.settlement_address,
852-
)
853-
854-
# Calculate permit amount for BUY orders:
855-
# (size * price / 1e6) + 1% fee + 10% safety margin
856-
buyer_cost = (ORDER_SIZE * bid_price) // 1_000_000
857-
total_fee = ORDER_SIZE // 100 # 1% fee
858-
permit_amount = ((buyer_cost + total_fee) * 110) // 100
859-
860-
# Sign and attach USDC permit
861-
permit = self.client.sign_usdc_permit(
862-
value=permit_amount,
863-
settlement_address=self.settlement_address,
864-
)
865-
bid_order.permit_signature = permit
866-
867-
result = self.client.post_order(bid_order)
868-
869-
# Place ask (sell YES)
870-
ask_order = self.client.create_limit_sell(
871-
market_id=self.market_id,
872-
outcome=Outcome.YES,
873-
price=ask_price,
874-
size=ORDER_SIZE,
875-
expiration=int(time.time()) + QUOTE_REFRESH_SECONDS + 60,
876-
settlement_address=self.settlement_address,
877-
)
878-
879-
# Calculate permit amount for SELL orders: size + 10% margin
880-
permit_amount = (ORDER_SIZE * 110) // 100
881-
882-
permit = self.client.sign_usdc_permit(
883-
value=permit_amount,
884-
settlement_address=self.settlement_address,
885-
)
886-
ask_order.permit_signature = permit
887-
888-
result = self.client.post_order(ask_order)
745+
# Sign and submit gasless max permit via relayer
746+
result = self.client.approve_usdc_for_settlement(settlement_address)
747+
# Wait for TX confirmation...
748+
self.approved_settlements[settlement_address] = 2**256 - 1
889749
```
890750

891751
**Key points:**
892-
- BUY orders need permit for: `(size * price / 1e6) + fee`
893-
- SELL orders need permit for: `size` (for JIT token splitting)
894-
- Always add a 10% safety margin to permit amounts
895-
- Permits are signed per-order with the settlement address as spender
896-
897-
### Pre-Approval Order Placement (Mode B)
898752

899-
When using pre-approval mode, orders are submitted **without** permit signatures:
900-
901-
```python
902-
# Bot initialization includes approval tracking
903-
class PreapprovedBot:
904-
def __init__(self, client: TurbineClient):
905-
self.client = client
906-
self.approved_settlements: dict[str, int] = {} # settlement -> approved amount
907-
self.order_size_usdc = 1.0 # Order size in USDC terms
908-
self.position_usdc: dict[str, float] = {} # market_id -> USDC position
909-
# ... other fields
910-
911-
def calculate_shares_from_usdc(self, usdc_amount: float, price: int) -> int:
912-
"""Convert USDC amount to shares based on price.
913-
914-
Args:
915-
usdc_amount: Amount in USDC (e.g., 1.0 = $1)
916-
price: Price in 6 decimals (e.g., 500000 = 50%)
917-
918-
Returns:
919-
Number of shares in 6 decimals
920-
"""
921-
# shares = (usdc * 1e6 * 1e6) / price
922-
return int((usdc_amount * 1_000_000 * 1_000_000) / price)
923-
924-
async def place_order(self, outcome: Outcome, price: int) -> None:
925-
"""Place order without permit signature (pre-approved)."""
926-
# Ensure approved for this market's settlement
927-
self.ensure_settlement_approved(self.settlement_address)
928-
929-
# Calculate shares from USDC amount
930-
shares = self.calculate_shares_from_usdc(self.order_size_usdc, price)
931-
932-
order = self.client.create_limit_buy(
933-
market_id=self.market_id,
934-
outcome=outcome,
935-
price=price,
936-
size=shares,
937-
expiration=int(time.time()) + 300,
938-
settlement_address=self.settlement_address,
939-
)
940-
941-
# NO permit_signature attached - uses pre-approved allowance
942-
result = self.client.post_order(order)
943-
print(f"Order submitted: {result.order_hash[:16]}...")
944-
```
945-
946-
**Key points:**
947753
- Call `ensure_settlement_approved()` when entering each new market
948-
- Orders submitted without `permit_signature` field
949-
- Settlement contract uses existing USDC allowance
754+
- Orders submitted without `permit_signature` field — Settlement uses existing allowance
755+
- No native gas token required (relayer pays gas)
756+
- One-time per settlement contract — all future orders reuse the allowance
950757
- Order size specified in USDC terms, converted to shares based on price
951-
- Track position in USDC terms per market for risk management
952-
953-
**When to auto-approve:**
954-
- At bot startup
955-
- When switching to a new market (new settlement address)
956-
- When allowance drops below threshold
957758

958759
## Automatic Winnings Claiming
959760

@@ -1142,9 +943,7 @@ After posting an order:
1142943
- **Market Expiration**: BTC 15-minute markets expire quickly. Bots handle this automatically!
1143944
- **Gas/Fees**: Trading on Polygon has minimal gas costs but watch for fees.
1144945
- **Continuous Operation**: Bots are designed to run 24/7, switching between markets automatically.
1145-
- **Approval Mode Choice**:
1146-
- **Permit mode**: Fully gasless, but ~5-10s delay between orders for nonce propagation
1147-
- **Pre-approval mode**: Needs native gas for approval TX, but no permit nonce delays
946+
- **USDC Approval**: Bots use a one-time gasless max permit per settlement contract. No native gas required.
1148947

1149948
## Quick Reference
1150949

0 commit comments

Comments
 (0)