diff --git a/.claude/skills/alphapoly-enter-position/SKILL.md b/.claude/skills/alphapoly-enter-position/SKILL.md
index 6584b30..0431630 100644
--- a/.claude/skills/alphapoly-enter-position/SKILL.md
+++ b/.claude/skills/alphapoly-enter-position/SKILL.md
@@ -11,13 +11,13 @@ Enter a hedged pair position (target + cover) from a detected alphapoly portfoli
1. **Backend running** on `http://localhost:8000`
2. **Wallet unlocked** — check `GET /wallet/status` for `"unlocked": true`; unlock via `POST /wallet/unlock` with `{"password": ""}`
-3. **Sufficient USDC.e balance** — total cost is `amount_per_position * 2`
+3. **Sufficient pUSD balance** — total cost is `amount_per_position * 2`
## 7-Step Flow
1. **List portfolios** — `GET /data/portfolios`; note `pair_id`, market IDs, and position sides
2. **Pick a pair** — select by `pair_id`
-3. **Set amount** — choose `amount_per_position` in USDC.e
+3. **Set amount** — choose `amount_per_position` in pUSD
4. **Estimate** (required) — `POST /trading/buy-pair/estimate`; show result to user; stop if `sufficient_balance: false`
5. **Confirm** — require explicit user approval before proceeding
6. **Execute** — `POST /trading/buy-pair`; check `success: true` on both legs; surface any `warnings`
@@ -27,7 +27,7 @@ For full request/response schemas, see [api-reference.md](api-reference.md).
## Safety Rules
-These trades involve real USDC on Polygon — transactions are irreversible once submitted on-chain. The estimate step is the only chance to catch problems before money moves.
+These trades involve real pUSD on Polygon — transactions are irreversible once submitted on-chain. The estimate step is the only chance to catch problems before money moves.
- Always run `/trading/buy-pair/estimate` before `/trading/buy-pair` — the estimate reveals actual costs, slippage, and balance issues before committing funds
- Never execute without explicit user confirmation — the user needs to see the estimate and agree to the cost
diff --git a/.claude/skills/alphapoly-enter-position/api-reference.md b/.claude/skills/alphapoly-enter-position/api-reference.md
index 45b7a9b..510e178 100644
--- a/.claude/skills/alphapoly-enter-position/api-reference.md
+++ b/.claude/skills/alphapoly-enter-position/api-reference.md
@@ -6,7 +6,7 @@
```
GET /wallet/status
```
-Response: `{ "exists": true, "unlocked": true, "address": "0x...", "balances": { "usdc_e": 150.0, "pol": 0.8 }, "approvals_set": true }`
+Response: `{ "exists": true, "unlocked": true, "address": "0x...", "balances": { "pusd": 150.0, "pol": 0.8 }, "approvals_set": true }`
### Unlock
```
@@ -75,7 +75,7 @@ Same request body as estimate. Response (`BuyPairResponse`):
},
"cover": { "...same shape..." },
"total_spent": 20.0,
- "final_balances": { "usdc_e": 130.0, "pol": 0.8 },
+ "final_balances": { "pusd": 130.0, "pol": 0.8 },
"warnings": []
}
```
diff --git a/.claude/skills/alphapoly-exit-position/api-reference.md b/.claude/skills/alphapoly-exit-position/api-reference.md
index bbbd176..02f575e 100644
--- a/.claude/skills/alphapoly-exit-position/api-reference.md
+++ b/.claude/skills/alphapoly-exit-position/api-reference.md
@@ -49,7 +49,7 @@ Response (`SellTokenResponse`):
## Merge Tokens
-Burns equal amounts of YES+NO tokens and returns USDC.e collateral. Use when a market has resolved. Merged amount = `min(yes_balance, no_balance)`.
+Burns equal amounts of YES+NO tokens and returns pUSD collateral. Use when a market has resolved. Merged amount = `min(yes_balance, no_balance)`.
```
POST /positions/{position_id}/merge
diff --git a/.env.example b/.env.example
index ddc6b64..946be40 100644
--- a/.env.example
+++ b/.env.example
@@ -44,11 +44,3 @@ CHAINSTACK_NODE=https://polygon-mainnet.core.chainstack.com/xxxxxxxx
# OPTIONAL: HTTPS proxy for Polymarket CLOB API
# Required if trading is geo-restricted in your region (403 errors)
# HTTPS_PROXY=http://user:pass@proxy-host:port
-
-# OPTIONAL: Polymarket V2 / V1 path selector. Post-2026-04-28 cutover the
-# bot defaults to V2 (pUSD collateral, py-clob-client-v2, EIP-712 v2). The
-# V1 endpoints stopped accepting orders at the cutover, so V2 is the only
-# path that actually trades. Set this to false only if you're running
-# against the legacy V1 stack via the v1-final tag — V1 paths will be
-# removed in v2.0 (~2026-05-05).
-# POLYMARKET_V2_ENABLED=true
diff --git a/CLAUDE.md b/CLAUDE.md
index 799f6ce..22d0bd6 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -2,13 +2,7 @@
> Polymarket alpha detection platform. LLM pipeline groups related markets, extracts logical implications, and builds covering portfolios (hedged positions via contrapositive logic). Next.js dashboard with real-time price tracking and position management.
-## Polymarket V2 cutover (2026-04-28 11:00 UTC)
-
-The bot supports both V1 (USDC.e collateral) and V2 (pUSD collateral) trading paths, gated by `POLYMARKET_V2_ENABLED`. Single source of truth: `core/feature_flags.v2_enabled()`.
-
-**Phase 2 PR #49** (`feat/v2-flip-default`) flips the default unset → true at cutover. **DO NOT MERGE BEFORE 2026-04-28 11:00 UTC** — V2 books are empty pre-cutover.
-
-Operator post-flip: re-run `WalletManager.set_approvals()` once (approvals are mode-gated). USDC.e holders wrap to pUSD via `experiments/trading/02_wrap_to_pusd.py`. V1 retirement (#37) is queued for ~2026-05-05 after a 1-week soak; the `v1-final` git tag at `94e89ce` is the permanent legacy reference.
+Trades against Polymarket V2 (pUSD collateral, `py-clob-client-v2`, EIP-712 domain version "2"). Legacy V1 was retired on 2026-04-28; for archaeology see the `v1-final` git tag.
## Commands
@@ -72,11 +66,11 @@ data/ # Pipeline outputs (gitignored)
| `POST /positions/{id}/exit` | Orchestrated sell + merge fallback |
| `POST /trading/buy-pair` | Execute covered pair trade |
| `POST /trading/buy-pair/estimate` | Estimate trade cost |
-| `GET /wallet/status` | Wallet state (balances, approvals, V2-aware) |
+| `GET /wallet/status` | Wallet state (POL + pUSD balances, approvals) |
| `POST /wallet/generate` | Generate new wallet (encrypted with passphrase) |
| `POST /wallet/import` | Import private key (encrypted with passphrase) |
| `POST /wallet/unlock` / `lock` | Decrypt key into memory / clear |
-| `POST /wallet/approve-contracts` | Set all Polymarket approvals (V2-aware) |
+| `POST /wallet/approve-contracts` | Set all Polymarket V2 approvals |
| `GET /health` | Health check |
> Debug: `GET /prices/current`, `WS /prices/ws`
@@ -109,7 +103,6 @@ VALIDATION_MODEL=... # Required: model for validation
POLYMARKET_TAG=politics # Optional: market filter
MARKET_POLLING_ENABLED=false # Optional: background polling
CHAINSTACK_NODE=https://... # Optional: Polygon RPC for on-chain trades
-POLYMARKET_V2_ENABLED=true # Optional: V1/V2 path selector. Pre-#49 default false; post-#49 default true. Set =false explicitly to opt back to V1 (until V1 is removed in #37).
```
## Git
diff --git a/README.md b/README.md
index 1043349..1b76814 100644
--- a/README.md
+++ b/README.md
@@ -15,10 +15,7 @@
# Alphapoly - Polymarket alpha detection platform
-> ⚠️ **Polymarket V2 cutover: 2026-04-28 11:00 UTC.**
-> After cutover, set `POLYMARKET_V2_ENABLED=true` in your `.env` and re-run `WalletManager.set_approvals()` once (approvals are mode-gated).
-> If you hold USDC.e, wrap it to pUSD with [`experiments/trading/02_wrap_to_pusd.py`](experiments/trading/02_wrap_to_pusd.py) before trading.
-> V1 code paths will be removed in **v2.0** (~2026-05-05). To pin to the legacy V1 stack: `git checkout v1-final`.
+> Trades against Polymarket V2 (pUSD collateral). The legacy V1 (USDC.e) stack was retired on 2026-04-28 — `git checkout v1-final` to inspect it. If you hold USDC.e, wrap it to pUSD with [`experiments/trading/02_wrap_to_pusd.py`](experiments/trading/02_wrap_to_pusd.py) before trading.
Find covering portfolios across correlated prediction markets using predefined rules and LLM decisions. The system detects relationships between markets, classifies them to identify hedging pairs, and tracks their prices. The platform offers a smooth UI for entering detected pairs when profit opportunities exist and tracking your positions.
@@ -113,7 +110,7 @@ Standalone research scripts (no imports from `backend/`). Three groups:
| Folder | Description |
|--------|-------------|
| [`experiments/`](experiments/) | Pipeline-step learning examples — fetch events, build groups, extract implications, validate, score portfolios, stream prices. Mirrors what `backend/core/runner.py` orchestrates, one stage per file. |
-| [`experiments/trading/`](experiments/trading/) | Wallet + funding + position helpers. Generate a wallet, swap native USDC → USDC.e (legacy), wrap USDC.e → pUSD (Polymarket V2 collateral), buy a position, transfer tokens. Flag-aware: scripts read `POLYMARKET_V2_ENABLED` to mirror the backend's V1/V2 routing. |
+| [`experiments/trading/`](experiments/trading/) | Wallet + funding + position helpers. Generate a wallet, swap native USDC → USDC.e (legacy), wrap USDC.e → pUSD (Polymarket V2 collateral), buy a position, transfer tokens. |
| [`experiments/onchain-otc/`](experiments/onchain-otc/) | On-chain OTC trading without the CLOB — split/merge, P2P transfers, atomic escrow, NegRisk conversions, and intent-based settlement on an Anvil fork of Polygon. Forked-chain research; uses USDC.e collateral (the fork's frozen state predates Polymarket V2 / pUSD). |
---
diff --git a/backend/core/feature_flags.py b/backend/core/feature_flags.py
deleted file mode 100644
index 767eb73..0000000
--- a/backend/core/feature_flags.py
+++ /dev/null
@@ -1,24 +0,0 @@
-"""Process-wide feature flags for the Polymarket V1 -> V2 cutover.
-
-Centralizes env-flag parsing so collateral plumbing, CLOB clients, and any
-future V2 paths read the same source of truth. The single helper here is
-re-exported from ``core.trading.clob_client`` for backward compatibility.
-"""
-
-import os
-
-
-def v2_enabled() -> bool:
- """Read the ``POLYMARKET_V2_ENABLED`` flag.
-
- Defaults to **True** post-2026-04-28 cutover — Polymarket V1 endpoints
- stopped accepting orders. Users on the legacy V1 stack must explicitly
- set ``POLYMARKET_V2_ENABLED=false``. The V1 code path will be removed in
- v2.0 (~2026-05-05); ``v1-final`` tag is the permanent legacy reference.
- """
- return os.environ.get("POLYMARKET_V2_ENABLED", "true").strip().lower() in (
- "1",
- "true",
- "yes",
- "on",
- )
diff --git a/backend/core/positions/manager.py b/backend/core/positions/manager.py
index 3b7a578..9d46de2 100644
--- a/backend/core/positions/manager.py
+++ b/backend/core/positions/manager.py
@@ -8,19 +8,13 @@
from web3 import Web3
-from core.feature_flags import v2_enabled
from core.http_retry import fetch_json_with_retry
from core.positions.service import PositionService
from core.positions.storage import PositionStorage
-from core.wallet.contracts import CONTRACTS, V2_CONTRACTS, CTF_ABI
+from core.wallet.contracts import CONTRACTS, CTF_ABI
from core.wallet.manager import WalletManager
-def _collateral_address() -> str:
- """Active collateral token (pUSD under V2 flag, USDC.e otherwise)."""
- return V2_CONTRACTS["PUSD"] if v2_enabled() else CONTRACTS["USDC_E"]
-
-
@dataclass
class SellResult:
"""Result of selling tokens via CLOB."""
@@ -101,30 +95,22 @@ def _merge_tokens(
self,
condition_id: str,
amount: float,
- neg_risk: bool = False,
) -> tuple[Optional[str], Optional[str]]:
- """Merge YES+NO tokens back to USDC. Returns (tx_hash, error).
+ """Merge YES+NO tokens back to pUSD. Returns (tx_hash, error).
- For binary markets, merges via CTF directly.
- For NegRisk (multi-outcome) markets, merges via the NegRisk adapter.
+ Always routes through the standard CTF: the per-outcome conditionId
+ from Gamma is registered on CTF. Gamma's `negRisk` flag is a market
+ grouping hint, not an on-chain routing instruction.
"""
w3 = self._get_web3()
address = Web3.to_checksum_address(self.wallet.address)
account = w3.eth.account.from_key(self.wallet.get_unlocked_key())
- # Always merge via standard CTF: the per-outcome conditionId from
- # Gamma is registered on CTF. Same reasoning as the split path in
- # `executor.py` — Gamma's `negRisk` flag is a market grouping hint,
- # not an on-chain routing instruction.
- merge_contract_addr = CONTRACTS["CTF"]
contract = w3.eth.contract(
- address=Web3.to_checksum_address(merge_contract_addr),
+ address=Web3.to_checksum_address(CONTRACTS["CTF"]),
abi=CTF_ABI,
)
- logger.info(
- f"Merge via CTF{' (NegRisk-grouped market)' if neg_risk else ''}: "
- f"{merge_contract_addr[:10]}..."
- )
+ logger.info(f"Merge via CTF: {CONTRACTS['CTF'][:10]}...")
amount_wei = int(amount * 1e6)
condition_bytes = bytes.fromhex(
@@ -135,17 +121,21 @@ def _merge_tokens(
base_gas_price = w3.eth.gas_price
gas_price = int(base_gas_price * 1.2)
- tx = contract.functions.mergePositions(
- Web3.to_checksum_address(_collateral_address()),
+ # Estimate gas live — see executor._split_position for context.
+ merge_fn = contract.functions.mergePositions(
+ Web3.to_checksum_address(CONTRACTS["PUSD"]),
bytes(32), # parentCollectionId
condition_bytes,
[1, 2], # partition for YES, NO
amount_wei,
- ).build_transaction(
+ )
+ gas_estimate = int(merge_fn.estimate_gas({"from": address}) * 1.25)
+
+ tx = merge_fn.build_transaction(
{
"from": address,
"nonce": w3.eth.get_transaction_count(address),
- "gas": 300000,
+ "gas": gas_estimate,
"gasPrice": gas_price,
"chainId": 137,
}
@@ -325,9 +315,7 @@ async def merge_position_tokens(
error=f"Failed to fetch market info: {e}",
)
- # Execute merge — use NegRisk adapter for multi-outcome markets
- neg_risk = bool(market_data.get("negRisk", False))
- tx_hash, error = self._merge_tokens(condition_id, mergeable, neg_risk=neg_risk)
+ tx_hash, error = self._merge_tokens(condition_id, mergeable)
if error:
return MergeResult(
diff --git a/backend/core/trading/clob.py b/backend/core/trading/clob.py
index c834887..8e08814 100644
--- a/backend/core/trading/clob.py
+++ b/backend/core/trading/clob.py
@@ -1,10 +1,9 @@
"""Shared CLOB market sell — FAK order with slippage protection.
-Under ``POLYMARKET_V2_ENABLED`` the V2 SDK is used. The V2 ``MarketOrderArgsV2``
-struct drops ``fee_rate_bps``/``nonce``/``taker`` (taker fees are computed
-server-side at match time in V2) and adds an optional ``metadata`` field. The
-EIP-712 domain version flips from ``"1"`` to ``"2"`` internally in
-``py_clob_client_v2`` — we don't pass it.
+Uses V2 SDK exclusively post-2026-04-28 cutover. The ``MarketOrderArgsV2``
+struct has no ``fee_rate_bps``/``nonce``/``taker`` (taker fees are computed
+server-side at match time) and adds an optional ``metadata`` field. The
+EIP-712 domain version is "2", handled internally by ``py_clob_client_v2``.
"""
import time
@@ -12,8 +11,6 @@
from loguru import logger
-from core.feature_flags import v2_enabled
-
def _tick_decimals(tick_size: float) -> int:
"""Count decimal places from a tick size value."""
@@ -56,18 +53,14 @@ def sell_via_clob(
return None, 0.0, msg
try:
- if v2_enabled():
- # V2 struct: no fee_rate_bps / nonce / taker; fees are match-time.
- # `metadata` defaults to BYTES32_ZERO; `builder_code` is set on the
- # client via builder_config (we leave it None per #27 scope).
- from py_clob_client_v2.clob_types import (
- MarketOrderArgsV2 as MarketOrderArgs,
- OrderType,
- )
- from py_clob_client_v2.order_builder.constants import SELL
- else:
- from py_clob_client.clob_types import MarketOrderArgs, OrderType
- from py_clob_client.order_builder.constants import SELL
+ # V2 struct: no fee_rate_bps / nonce / taker; fees are match-time.
+ # `metadata` defaults to BYTES32_ZERO; `builder_code` is set on the
+ # client via builder_config (we leave it None per #27 scope).
+ from py_clob_client_v2.clob_types import (
+ MarketOrderArgsV2 as MarketOrderArgs,
+ OrderType,
+ )
+ from py_clob_client_v2.order_builder.constants import SELL
# Fetch market's tick size for correct price precision
try:
diff --git a/backend/core/trading/clob_client.py b/backend/core/trading/clob_client.py
index 5caf4c3..9d4f460 100644
--- a/backend/core/trading/clob_client.py
+++ b/backend/core/trading/clob_client.py
@@ -1,9 +1,8 @@
-"""Shared CLOB client initialization with proxy support.
+"""CLOB V2 client initialization with proxy support.
-Both V1 (current prod) and V2 (post-cutover) clients live here so we can flip
-between them with a single env flag during the 2026-04-28 cutover. The V2
-init is parallel and unused by production call sites until issue #36 wires it
-in. Set ``POLYMARKET_V2_ENABLED=true`` to opt in.
+Polymarket V1 endpoints stopped accepting orders at the 2026-04-28 cutover.
+The bot uses ``py_clob_client_v2`` exclusively (pUSD collateral, EIP-712
+domain version "2", server-side fee computation at match time).
"""
import os
@@ -14,23 +13,7 @@
from core.wallet.manager import WalletManager
-# Post-2026-04-28 cutover: Polymarket consolidated clob-v2.polymarket.com
-# into clob.polymarket.com (the old subdomain now 301-redirects, which the
-# V2 SDK's http client does not follow, so we point at the canonical host).
-CLOB_V2_URL = "https://clob.polymarket.com"
-CLOB_V1_URL = "https://clob.polymarket.com"
-
-
-def _v2_enabled() -> bool:
- """Read the V2 feature flag. Defaults to False (V1 stays in charge).
-
- Thin wrapper around :func:`core.feature_flags.v2_enabled` — kept for
- backward compatibility with call sites that imported the underscore name.
- New code should import from :mod:`core.feature_flags` directly.
- """
- from core.feature_flags import v2_enabled
-
- return v2_enabled()
+CLOB_URL = "https://clob.polymarket.com"
def _apply_proxy(clob_helpers_module) -> None:
@@ -52,72 +35,17 @@ def _apply_proxy(clob_helpers_module) -> None:
)
-def _get_clob_client_v1(wallet: WalletManager) -> Optional[object]:
- """Initialize legacy V1 CLOB client (USDC.e collateral)."""
+def get_clob_client(wallet: WalletManager) -> Optional[object]:
+ """Initialize V2 CLOB client. Returns None on failure."""
try:
- from py_clob_client.client import ClobClient
- import py_clob_client.http_helpers.helpers as clob_helpers
+ from py_clob_client_v2.client import ClobClient
+ import py_clob_client_v2.http_helpers.helpers as clob_helpers
except ImportError:
- logger.error("py-clob-client not installed")
+ logger.error("py-clob-client-v2 not installed")
return None
_apply_proxy(clob_helpers)
- try:
- private_key = wallet.get_unlocked_key()
- address = wallet.address
- if not address:
- logger.error("Wallet address is not set")
- return None
- client = ClobClient(
- CLOB_V1_URL,
- key=private_key,
- chain_id=137,
- signature_type=0,
- funder=address,
- )
- creds = client.create_or_derive_api_creds()
- client.set_api_creds(creds)
- return client
- except Exception as e:
- logger.error(f"CLOB API error: {e}")
- return None
-
-
-def _get_clob_client_v2(wallet: WalletManager) -> Optional[object]:
- """Initialize V2 CLOB client (pUSD collateral, EIP-712 domain version "2").
-
- Constructed with the V2 options-object signature and ``chain="polygon"``.
- No production call site reaches this yet — it ships behind the
- ``POLYMARKET_V2_ENABLED`` flag for the 2026-04-28 cutover (#36 flips it).
-
- Manual smoke test for the signing path (no submission)::
-
- cd backend && POLYMARKET_V2_ENABLED=true uv run python -c "
- from core.wallet.manager import WalletManager
- from core.trading.clob_client import get_clob_client
- w = WalletManager(); w.unlock(input('passphrase: '))
- c = get_clob_client(w)
- print('client:', c)
- # Build (do not post) an order to exercise EIP-712 v2 signing:
- # from py_clob_client_v2.clob_types import OrderArgs
- # args = OrderArgs(price=0.5, size=5, side='BUY', token_id='')
- # print(c.create_order(args))
- "
-
- The "do not post" rule: stop after ``create_order``; never call
- ``post_order``. We just want to verify the EIP-712 v2 domain signs cleanly
- against ``clob-v2.polymarket.com``.
- """
- try:
- from py_clob_client_v2.client import ClobClient as ClobClientV2
- import py_clob_client_v2.http_helpers.helpers as clob_helpers_v2
- except ImportError:
- logger.error("py-clob-client-v2 not installed (POLYMARKET_V2_ENABLED set)")
- return None
-
- _apply_proxy(clob_helpers_v2)
-
try:
private_key = wallet.get_unlocked_key()
address = wallet.address
@@ -126,15 +54,14 @@ def _get_clob_client_v2(wallet: WalletManager) -> Optional[object]:
return None
# V2 keeps `chain_id` (still int 137 for Polygon) and handles EIP-712
# domain version "2" internally. `builder_config=None` per #27 scope.
- client = ClobClientV2(
- host=CLOB_V2_URL,
+ client = ClobClient(
+ host=CLOB_URL,
chain_id=137,
key=private_key,
signature_type=0,
funder=address,
builder_config=None,
)
- # V2 split V1's `create_or_derive_api_creds` into separate methods.
# `derive_api_key` is idempotent for an existing key; the V2 SDK
# raises `PolyApiException` when the address has no key yet — narrow
# the catch so transient network errors fail visibly rather than
@@ -148,17 +75,5 @@ def _get_clob_client_v2(wallet: WalletManager) -> Optional[object]:
client.set_api_creds(creds)
return client
except Exception as e:
- logger.error(f"CLOB V2 API error: {e}")
+ logger.error(f"CLOB API error: {e}")
return None
-
-
-def get_clob_client(wallet: WalletManager) -> Optional[object]:
- """Initialize CLOB client. Picks V2 if ``POLYMARKET_V2_ENABLED`` is set.
-
- Default is V1 — the V2 path is dormant until the 2026-04-28 cutover.
- Returns None on failure (missing dep, missing wallet, API error).
- """
- if _v2_enabled():
- logger.info("POLYMARKET_V2_ENABLED set — using V2 CLOB client")
- return _get_clob_client_v2(wallet)
- return _get_clob_client_v1(wallet)
diff --git a/backend/core/trading/executor.py b/backend/core/trading/executor.py
index 74d48ef..7a5b485 100644
--- a/backend/core/trading/executor.py
+++ b/backend/core/trading/executor.py
@@ -9,17 +9,11 @@
from web3 import Web3
from loguru import logger
-from core.feature_flags import v2_enabled
from core.http_retry import fetch_json_with_retry
-from core.wallet.contracts import CONTRACTS, V2_CONTRACTS, CTF_ABI
+from core.wallet.contracts import CONTRACTS, CTF_ABI
from core.wallet.manager import WalletManager
-def _collateral_address() -> str:
- """Active collateral token (pUSD under V2 flag, USDC.e otherwise)."""
- return V2_CONTRACTS["PUSD"] if v2_enabled() else CONTRACTS["USDC_E"]
-
-
@dataclass
class MarketInfo:
market_id: str
@@ -124,34 +118,23 @@ def _split_position(
self,
condition_id: str,
amount_usd: float,
- neg_risk: bool = False,
) -> tuple[str, list[str]]:
- """Split USDC into YES + NO tokens. Returns (tx_hash, [ctf_token_ids]).
+ """Split pUSD into YES + NO tokens. Returns (tx_hash, [ctf_token_ids]).
- For binary markets, splits via CTF directly.
- For NegRisk (multi-outcome) markets, splits via the NegRisk adapter
- so minted tokens match CLOB token IDs.
+ Always routes through the standard CTF: the per-outcome conditionId
+ from Gamma is registered on CTF (verified via getOutcomeSlotCount).
+ Gamma's `negRisk` flag is a market grouping hint, not an on-chain
+ routing instruction.
"""
w3 = self._get_web3()
address = Web3.to_checksum_address(self.wallet.address)
account = w3.eth.account.from_key(self.wallet.get_unlocked_key())
- # Always route splits through the standard CTF: the per-outcome
- # conditionId from Gamma is registered on CTF (verified via
- # getOutcomeSlotCount). The V1 NegRiskAdapter's 5-arg splitPosition
- # would still resolve, but its 2-arg native variant requires a
- # NegRisk-prepared questionId we don't have, and on V2 the adapter
- # rejects pUSD anyway. The `neg_risk` flag on Gamma is a market
- # grouping hint, not an on-chain routing instruction for splits.
- split_contract_addr = CONTRACTS["CTF"]
ctf = w3.eth.contract(
- address=Web3.to_checksum_address(split_contract_addr),
+ address=Web3.to_checksum_address(CONTRACTS["CTF"]),
abi=CTF_ABI,
)
- logger.info(
- f"Split via CTF{' (NegRisk-grouped market)' if neg_risk else ''}: "
- f"{split_contract_addr[:10]}..."
- )
+ logger.info(f"Split via CTF: {CONTRACTS['CTF'][:10]}...")
amount_wei = int(amount_usd * 1e6)
condition_bytes = bytes.fromhex(
@@ -162,17 +145,24 @@ def _split_position(
base_gas_price = w3.eth.gas_price
gas_price = int(base_gas_price * 1.2)
- tx = ctf.functions.splitPosition(
- Web3.to_checksum_address(_collateral_address()),
+ # Estimate gas live — Chainstack rejects "gas too high" with the old
+ # hardcoded 300000 (a split needs ~135k, merge ~90k). Estimate + 25%
+ # headroom keeps the cap close to actual usage but tolerates state
+ # changes between estimate and submit.
+ split_fn = ctf.functions.splitPosition(
+ Web3.to_checksum_address(CONTRACTS["PUSD"]),
bytes(32), # parentCollectionId
condition_bytes,
[1, 2], # partition for YES, NO
amount_wei,
- ).build_transaction(
+ )
+ gas_estimate = int(split_fn.estimate_gas({"from": address}) * 1.25)
+
+ tx = split_fn.build_transaction(
{
"from": address,
"nonce": w3.eth.get_transaction_count(address),
- "gas": 300000,
+ "gas": gas_estimate,
"gasPrice": gas_price,
"chainId": 137,
}
@@ -224,9 +214,7 @@ async def buy_single_position(
# Split position
try:
- split_tx, ctf_token_ids = self._split_position(
- market.condition_id, amount, neg_risk=market.neg_risk
- )
+ split_tx, ctf_token_ids = self._split_position(market.condition_id, amount)
except Exception as e:
return TradeResult(
success=False,
@@ -332,12 +320,11 @@ def _fee_stub(info: MarketInfo) -> dict:
balances = self.wallet.get_balances()
required = amount_per_position * 2 + entry_fees
- if balances.usdc_e < required:
- collateral_label = "pUSD" if v2_enabled() else "USDC.e"
+ if balances.pusd < required:
raise ValueError(
- f"Insufficient {collateral_label}: need {required:.2f} "
+ f"Insufficient pUSD: need {required:.2f} "
f"(includes ${entry_fees:.2f} taker fees), "
- f"have {balances.usdc_e:.2f}"
+ f"have {balances.pusd:.2f}"
)
# Buy target position
@@ -371,6 +358,6 @@ def _fee_stub(info: MarketInfo) -> dict:
total_spent=amount_per_position * 2,
final_balances={
"pol": final_balances.pol,
- "usdc_e": final_balances.usdc_e,
+ "pusd": final_balances.pusd,
},
)
diff --git a/backend/core/wallet/contracts.py b/backend/core/wallet/contracts.py
index 540bf53..bf488b8 100644
--- a/backend/core/wallet/contracts.py
+++ b/backend/core/wallet/contracts.py
@@ -1,21 +1,12 @@
-"""Polymarket contract addresses and ABIs."""
+"""Polymarket contract addresses and ABIs (V2-only post-2026-04-28 cutover)."""
-# Polygon mainnet contracts
+# Polygon mainnet — Polymarket V2 contracts (pUSD collateral).
+# Confirmed against Polymarket docs 2026-04-26. CollateralOnramp wraps USDC.e
+# -> pUSD; CollateralOfframp unwraps. PUSD is a proxy; PUSD_IMPL is impl.
CONTRACTS = {
- "USDC_E": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174",
"CTF": "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045",
- "CTF_EXCHANGE": "0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E",
- "NEG_RISK_CTF_EXCHANGE": "0xC5d563A36AE78145C45a50134d48A1215220f80a",
- "NEG_RISK_ADAPTER": "0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296",
-}
-
-# V2 contracts for the 2026-04-28 Polymarket cutover.
-# Confirmed against Polymarket docs on 2026-04-26. pUSD replaces USDC.e as
-# collateral; CTF exchange addresses are new. Onramp wraps USDC.e -> pUSD,
-# offramp unwraps. PUSD is a proxy at PUSD; PUSD_IMPL is the implementation.
-V2_CONTRACTS = {
- "CTF_EXCHANGE_V2": "0xE111180000d2663C0091e4f400237545B87B996B",
- "NEG_RISK_CTF_EXCHANGE_V2": "0xe2222d279d744050d28e00520010520000310F59",
+ "CTF_EXCHANGE": "0xE111180000d2663C0091e4f400237545B87B996B",
+ "NEG_RISK_CTF_EXCHANGE": "0xe2222d279d744050d28e00520010520000310F59",
"PUSD": "0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB",
"PUSD_IMPL": "0x6bBCef9f7ef3B6C592c99e0f206a0DE94Ad0925f",
"COLLATERAL_ONRAMP": "0x93070a847efEf7F70739046A929D47a521F5B8ee",
diff --git a/backend/core/wallet/manager.py b/backend/core/wallet/manager.py
index 8b3db1c..bc30852 100644
--- a/backend/core/wallet/manager.py
+++ b/backend/core/wallet/manager.py
@@ -7,30 +7,14 @@
from web3 import Web3
from loguru import logger
-from core.feature_flags import v2_enabled
from core.wallet.storage import WalletStorage
-from core.wallet.contracts import CONTRACTS, V2_CONTRACTS, ERC20_ABI, CTF_ABI
-
-
-def _collateral_address() -> str:
- """Active collateral token address (pUSD under V2 flag, USDC.e otherwise)."""
- return V2_CONTRACTS["PUSD"] if v2_enabled() else CONTRACTS["USDC_E"]
-
-
-def _exchange_addresses() -> tuple[str, str]:
- """(CTF exchange, NegRisk CTF exchange) addresses for the active env."""
- if v2_enabled():
- return (
- V2_CONTRACTS["CTF_EXCHANGE_V2"],
- V2_CONTRACTS["NEG_RISK_CTF_EXCHANGE_V2"],
- )
- return (CONTRACTS["CTF_EXCHANGE"], CONTRACTS["NEG_RISK_CTF_EXCHANGE"])
+from core.wallet.contracts import CONTRACTS, ERC20_ABI, CTF_ABI
@dataclass
class WalletBalances:
pol: float
- usdc_e: float # Holds pUSD balance when POLYMARKET_V2_ENABLED is set.
+ pusd: float
@dataclass
@@ -99,7 +83,7 @@ def lock(self) -> None:
logger.info("Wallet locked")
def get_balances(self) -> WalletBalances:
- """Get POL and USDC.e balances."""
+ """Get POL and pUSD balances."""
address = self.address
if not address:
raise ValueError("No wallet configured")
@@ -109,14 +93,14 @@ def get_balances(self) -> WalletBalances:
pol = float(w3.from_wei(w3.eth.get_balance(checksum), "ether"))
- # pUSD when V2 flag is set, USDC.e otherwise. Both are 6-decimal ERC-20s.
- collateral = w3.eth.contract(
- address=Web3.to_checksum_address(_collateral_address()),
+ # pUSD is a 6-decimal ERC-20 (Polymarket V2 collateral).
+ pusd = w3.eth.contract(
+ address=Web3.to_checksum_address(CONTRACTS["PUSD"]),
abi=ERC20_ABI,
)
- collateral_balance = collateral.functions.balanceOf(checksum).call() / 1e6
+ pusd_balance = pusd.functions.balanceOf(checksum).call() / 1e6
- return WalletBalances(pol=pol, usdc_e=collateral_balance)
+ return WalletBalances(pol=pol, pusd=pusd_balance)
def check_approvals(self) -> bool:
"""Check if all Polymarket approvals are set."""
@@ -127,8 +111,8 @@ def check_approvals(self) -> bool:
w3 = self._get_web3()
checksum = Web3.to_checksum_address(address)
- collateral = w3.eth.contract(
- address=Web3.to_checksum_address(_collateral_address()),
+ pusd = w3.eth.contract(
+ address=Web3.to_checksum_address(CONTRACTS["PUSD"]),
abi=ERC20_ABI,
)
ctf = w3.eth.contract(
@@ -136,25 +120,18 @@ def check_approvals(self) -> bool:
abi=CTF_ABI,
)
- ctf_exchange, neg_risk_exchange = _exchange_addresses()
- # NegRisk Adapter is unchanged in V2 (per Polymarket docs 2026-04-26).
- # We keep its approvals for compatibility, but the bot's actual splits
- # and merges always route through the standard CTF — see comments in
- # core/trading/executor.py and core/positions/manager.py.
collateral_spenders = [
CONTRACTS["CTF"],
- ctf_exchange,
- neg_risk_exchange,
- CONTRACTS["NEG_RISK_ADAPTER"],
+ CONTRACTS["CTF_EXCHANGE"],
+ CONTRACTS["NEG_RISK_CTF_EXCHANGE"],
]
ctf_operators = [
- ctf_exchange,
- neg_risk_exchange,
- CONTRACTS["NEG_RISK_ADAPTER"],
+ CONTRACTS["CTF_EXCHANGE"],
+ CONTRACTS["NEG_RISK_CTF_EXCHANGE"],
]
for spender in collateral_spenders:
- allowance = collateral.functions.allowance(checksum, spender).call()
+ allowance = pusd.functions.allowance(checksum, spender).call()
if allowance == 0:
return False
@@ -197,8 +174,8 @@ def set_approvals(self) -> list[str]:
address = Web3.to_checksum_address(self._address)
account = w3.eth.account.from_key(self._unlocked_key)
- collateral = w3.eth.contract(
- address=Web3.to_checksum_address(_collateral_address()),
+ pusd = w3.eth.contract(
+ address=Web3.to_checksum_address(CONTRACTS["PUSD"]),
abi=ERC20_ABI,
)
ctf = w3.eth.contract(
@@ -209,15 +186,12 @@ def set_approvals(self) -> list[str]:
MAX_UINT256 = 2**256 - 1
tx_hashes = []
- ctf_exchange, neg_risk_exchange = _exchange_addresses()
approvals = [
- (collateral, "approve", CONTRACTS["CTF"], MAX_UINT256),
- (collateral, "approve", ctf_exchange, MAX_UINT256),
- (collateral, "approve", neg_risk_exchange, MAX_UINT256),
- (collateral, "approve", CONTRACTS["NEG_RISK_ADAPTER"], MAX_UINT256),
- (ctf, "setApprovalForAll", ctf_exchange, True),
- (ctf, "setApprovalForAll", neg_risk_exchange, True),
- (ctf, "setApprovalForAll", CONTRACTS["NEG_RISK_ADAPTER"], True),
+ (pusd, "approve", CONTRACTS["CTF"], MAX_UINT256),
+ (pusd, "approve", CONTRACTS["CTF_EXCHANGE"], MAX_UINT256),
+ (pusd, "approve", CONTRACTS["NEG_RISK_CTF_EXCHANGE"], MAX_UINT256),
+ (ctf, "setApprovalForAll", CONTRACTS["CTF_EXCHANGE"], True),
+ (ctf, "setApprovalForAll", CONTRACTS["NEG_RISK_CTF_EXCHANGE"], True),
]
for contract, method, spender, value in approvals:
diff --git a/backend/pyproject.toml b/backend/pyproject.toml
index f6a4485..d2e83d8 100644
--- a/backend/pyproject.toml
+++ b/backend/pyproject.toml
@@ -16,9 +16,6 @@ dependencies = [
"watchfiles>=0.21.0",
# Trading (experiments)
"eth-account>=0.13.7",
- "py-clob-client>=0.34.5",
- # V2 client for the 2026-04-28 Polymarket cutover (parallel install).
- # Currently gated by POLYMARKET_V2_ENABLED; see core/trading/clob_client.py.
"py-clob-client-v2==1.0.0",
"web3>=7.15.0",
"cryptography>=46.0.7",
diff --git a/backend/server/routers/trading.py b/backend/server/routers/trading.py
index c9425b5..3f4dcd4 100644
--- a/backend/server/routers/trading.py
+++ b/backend/server/routers/trading.py
@@ -253,8 +253,8 @@ def _fee_stub(m) -> dict:
"position": req.cover_position,
"price": cover_price,
},
- wallet_balance=balances.usdc_e,
- sufficient_balance=balances.usdc_e >= net_cost,
+ wallet_balance=balances.pusd,
+ sufficient_balance=balances.pusd >= net_cost,
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
diff --git a/backend/server/routers/wallet.py b/backend/server/routers/wallet.py
index f1dcff5..23145d4 100644
--- a/backend/server/routers/wallet.py
+++ b/backend/server/routers/wallet.py
@@ -88,7 +88,7 @@ async def get_status():
unlocked=status.unlocked,
balances={
"pol": status.balances.pol,
- "usdc_e": status.balances.usdc_e,
+ "pusd": status.balances.pusd,
}
if status.balances
else None,
@@ -105,7 +105,7 @@ async def generate_wallet(req: PasswordRequest):
address = manager.generate(req.password)
return GenerateResponse(
address=address,
- message="Wallet created. Fund with POL and USDC.e, then set approvals.",
+ message="Wallet created. Fund with POL and pUSD, then set approvals.",
)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
diff --git a/backend/tests/test_v2_collateral.py b/backend/tests/test_v2_collateral.py
deleted file mode 100644
index 121d020..0000000
--- a/backend/tests/test_v2_collateral.py
+++ /dev/null
@@ -1,69 +0,0 @@
-"""Tests for V2 collateral plumbing — USDC.e <-> pUSD switch under POLYMARKET_V2_ENABLED."""
-
-import importlib
-
-import pytest
-
-from core import feature_flags
-from core.positions import manager as positions_manager
-from core.trading import executor as trading_executor
-from core.wallet import manager as wallet_manager
-from core.wallet.contracts import CONTRACTS, V2_CONTRACTS
-
-
-@pytest.fixture
-def v1(monkeypatch):
- # Post-cutover the default is V2. Opt-in to V1 explicitly.
- monkeypatch.setenv("POLYMARKET_V2_ENABLED", "false")
- importlib.reload(feature_flags)
-
-
-@pytest.fixture
-def v2(monkeypatch):
- monkeypatch.delenv("POLYMARKET_V2_ENABLED", raising=False)
- importlib.reload(feature_flags)
-
-
-class TestFeatureFlag:
- def test_default_is_true_post_cutover(self, v2):
- assert feature_flags.v2_enabled() is True
-
- def test_explicit_false_opts_into_v1(self, v1):
- assert feature_flags.v2_enabled() is False
-
- @pytest.mark.parametrize("val", ["1", "true", "True", "yes", "on"])
- def test_truthy_values(self, monkeypatch, val):
- monkeypatch.setenv("POLYMARKET_V2_ENABLED", val)
- assert feature_flags.v2_enabled() is True
-
- @pytest.mark.parametrize("val", ["", "0", "false", "no", "off", "anything"])
- def test_falsy_values(self, monkeypatch, val):
- # Empty string is an explicit empty value, NOT "unset" — only an
- # unset env var falls through to the "true" default
- # (covered by test_default_is_true_post_cutover).
- monkeypatch.setenv("POLYMARKET_V2_ENABLED", val)
- assert feature_flags.v2_enabled() is False
-
-
-class TestCollateralRouting:
- def test_v1_uses_usdc_e_everywhere(self, v1):
- assert wallet_manager._collateral_address() == CONTRACTS["USDC_E"]
- assert trading_executor._collateral_address() == CONTRACTS["USDC_E"]
- assert positions_manager._collateral_address() == CONTRACTS["USDC_E"]
-
- def test_v2_uses_pusd_everywhere(self, v2):
- assert wallet_manager._collateral_address() == V2_CONTRACTS["PUSD"]
- assert trading_executor._collateral_address() == V2_CONTRACTS["PUSD"]
- assert positions_manager._collateral_address() == V2_CONTRACTS["PUSD"]
-
-
-class TestExchangeRouting:
- def test_v1_targets_v1_exchanges(self, v1):
- ctf, neg = wallet_manager._exchange_addresses()
- assert ctf == CONTRACTS["CTF_EXCHANGE"]
- assert neg == CONTRACTS["NEG_RISK_CTF_EXCHANGE"]
-
- def test_v2_targets_v2_exchanges(self, v2):
- ctf, neg = wallet_manager._exchange_addresses()
- assert ctf == V2_CONTRACTS["CTF_EXCHANGE_V2"]
- assert neg == V2_CONTRACTS["NEG_RISK_CTF_EXCHANGE_V2"]
diff --git a/backend/tests/test_v2_order_struct.py b/backend/tests/test_v2_order_struct.py
index 831920f..98c8d88 100644
--- a/backend/tests/test_v2_order_struct.py
+++ b/backend/tests/test_v2_order_struct.py
@@ -1,48 +1,20 @@
-"""Tests for V2 order struct deltas (#36).
+"""Tests for V2 order struct contract.
-V1 (``py_clob_client.clob_types.MarketOrderArgs``) carries
-``fee_rate_bps``/``nonce``/``taker`` as fields on the signed order. V2
-(``py_clob_client_v2.clob_types.MarketOrderArgsV2``) drops all three and adds
-``metadata`` (bytes32). Taker fees are computed server-side at match time.
-
-These tests assert that the SDKs we ship satisfy the contract, and that
-``sell_via_clob`` reaches for the V2 struct under the feature flag.
+``py_clob_client_v2.clob_types.MarketOrderArgsV2`` has no
+``fee_rate_bps``/``nonce``/``taker`` fields (taker fees are server-side at
+match time) and adds ``metadata`` (bytes32). These tests assert the SDK we
+ship satisfies the contract, and that ``sell_via_clob`` reaches for the V2
+struct.
"""
-import importlib
from dataclasses import fields
from unittest.mock import MagicMock, patch
-import pytest
-
-from core import feature_flags
-
-
-@pytest.fixture
-def v1(monkeypatch):
- # Post-cutover the default is V2. Opt-in to V1 explicitly.
- monkeypatch.setenv("POLYMARKET_V2_ENABLED", "false")
- importlib.reload(feature_flags)
-
-
-@pytest.fixture
-def v2(monkeypatch):
- monkeypatch.delenv("POLYMARKET_V2_ENABLED", raising=False)
- importlib.reload(feature_flags)
-
class TestOrderStructDeltas:
- """Static field-level deltas between V1 and V2 SDKs."""
-
- def test_v1_market_order_has_legacy_fee_fields(self):
- from py_clob_client.clob_types import MarketOrderArgs
+ """Static field-level assertions for the V2 SDK."""
- names = {f.name for f in fields(MarketOrderArgs)}
- assert "fee_rate_bps" in names
- assert "nonce" in names
- assert "taker" in names
-
- def test_v2_market_order_drops_legacy_fee_fields(self):
+ def test_market_order_drops_legacy_fee_fields(self):
from py_clob_client_v2.clob_types import MarketOrderArgsV2
names = {f.name for f in fields(MarketOrderArgsV2)}
@@ -50,23 +22,14 @@ def test_v2_market_order_drops_legacy_fee_fields(self):
assert "nonce" not in names
assert "taker" not in names
- def test_v2_market_order_adds_metadata(self):
+ def test_market_order_adds_metadata(self):
from py_clob_client_v2.clob_types import MarketOrderArgsV2
names = {f.name for f in fields(MarketOrderArgsV2)}
assert "metadata" in names
- # builder_code is the V2 builder-attribution path (was also on V1, kept).
assert "builder_code" in names
- def test_v1_limit_order_has_legacy_fee_fields(self):
- from py_clob_client.clob_types import OrderArgs
-
- names = {f.name for f in fields(OrderArgs)}
- assert "fee_rate_bps" in names
- assert "nonce" in names
- assert "taker" in names
-
- def test_v2_limit_order_drops_legacy_fee_fields(self):
+ def test_limit_order_drops_legacy_fee_fields(self):
from py_clob_client_v2.clob_types import OrderArgsV2
names = {f.name for f in fields(OrderArgsV2)}
@@ -77,36 +40,8 @@ def test_v2_limit_order_drops_legacy_fee_fields(self):
class TestSellViaClobRouting:
- """``sell_via_clob`` must build the right struct under each flag."""
-
- @patch("core.trading.clob._get_filled_size")
- def test_v1_uses_v1_market_order_struct(self, mock_fill, v1):
- from core.trading import clob as clob_mod
-
- captured: dict = {}
-
- def fake_create_market_order(args):
- captured["args"] = args
- captured["module"] = type(args).__module__
- return MagicMock()
-
- client = MagicMock()
- client.create_market_order.side_effect = fake_create_market_order
- client.post_order.return_value = {"success": True, "orderID": "0xabc"}
- client.get_tick_size.return_value = 0.01
- mock_fill.return_value = 5.0
-
- clob_mod.sell_via_clob(client, "token123", 5.0, 0.5)
-
- assert captured["module"].startswith("py_clob_client.")
- assert not captured["module"].startswith("py_clob_client_v2.")
- # V1 struct exposes legacy fields (defaults zero/empty).
- assert hasattr(captured["args"], "fee_rate_bps")
- assert hasattr(captured["args"], "nonce")
- assert hasattr(captured["args"], "taker")
-
@patch("core.trading.clob._get_filled_size")
- def test_v2_uses_v2_market_order_struct(self, mock_fill, v2):
+ def test_uses_v2_market_order_struct(self, mock_fill):
from core.trading import clob as clob_mod
captured: dict = {}
@@ -125,7 +60,6 @@ def fake_create_market_order(args):
clob_mod.sell_via_clob(client, "token123", 5.0, 0.5)
assert captured["module"].startswith("py_clob_client_v2.")
- # Legacy fee fields are gone; metadata is the new addition.
assert not hasattr(captured["args"], "fee_rate_bps")
assert not hasattr(captured["args"], "nonce")
assert not hasattr(captured["args"], "taker")
diff --git a/backend/uv.lock b/backend/uv.lock
index 12effd6..922315a 100644
--- a/backend/uv.lock
+++ b/backend/uv.lock
@@ -119,7 +119,6 @@ dependencies = [
{ name = "fastapi" },
{ name = "httpx" },
{ name = "loguru" },
- { name = "py-clob-client" },
{ name = "py-clob-client-v2" },
{ name = "python-dotenv" },
{ name = "rich" },
@@ -136,7 +135,6 @@ requires-dist = [
{ name = "fastapi", specifier = ">=0.108.0" },
{ name = "httpx", specifier = ">=0.28.1" },
{ name = "loguru", specifier = ">=0.7.3" },
- { name = "py-clob-client", specifier = ">=0.34.5" },
{ name = "py-clob-client-v2", specifier = "==1.0.0" },
{ name = "python-dotenv", specifier = ">=1.2.2" },
{ name = "rich", specifier = ">=13.0.0" },
@@ -1193,37 +1191,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" },
]
-[[package]]
-name = "py-builder-signing-sdk"
-version = "0.0.2"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "python-dotenv" },
- { name = "requests" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/18/e3/fab3253021a4d766345f491f00e40d89c7c5640bf6d20d9f5a3182b482de/py_builder_signing_sdk-0.0.2.tar.gz", hash = "sha256:27fa4401944220c51809f549a263c6e53b5cd981fca98eda7ef6f21944e6bf94", size = 6411, upload-time = "2025-10-20T22:17:56.658Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/57/fb/23c68c8f6356a50f959e2df2ae80e8344c3ff8ccca92943848a57a495928/py_builder_signing_sdk-0.0.2-py3-none-any.whl", hash = "sha256:114b9d57bec228177d759ce15c475589f47db2252ed1fd67cac3c9b0640abe76", size = 9082, upload-time = "2025-10-20T22:17:55.629Z" },
-]
-
-[[package]]
-name = "py-clob-client"
-version = "0.34.5"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "eth-account" },
- { name = "eth-utils" },
- { name = "httpx", extra = ["http2"] },
- { name = "poly-eip712-structs" },
- { name = "py-builder-signing-sdk" },
- { name = "py-order-utils" },
- { name = "python-dotenv" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/fa/a4/64f81cd2ba674f575cb53a46be3fb14adc2ba92dd229ac0eb2c4a0456662/py_clob_client-0.34.5.tar.gz", hash = "sha256:fdc3907e3dae8e2e8bcc93a7c97780493a8d97e0e40247c84d2291ccb2d4e026", size = 38164, upload-time = "2026-01-13T17:13:32.037Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/85/64/1d5a1ecc0a95f1552da62ebd01d4b6bae63505c6ccba36632250d313445e/py_clob_client-0.34.5-py3-none-any.whl", hash = "sha256:31b7b57d522164f3eeab6bfdd78c42d8b313ff3d4469e0966fa2296121fbfb12", size = 43788, upload-time = "2026-01-13T17:13:30.877Z" },
-]
-
[[package]]
name = "py-clob-client-v2"
version = "1.0.0"
diff --git a/experiments/trading/01_setup_wallet.py b/experiments/trading/01_setup_wallet.py
index bd60b78..ade5d14 100644
--- a/experiments/trading/01_setup_wallet.py
+++ b/experiments/trading/01_setup_wallet.py
@@ -7,15 +7,11 @@
WHAT IT DOES:
1. Generates a new Ethereum-compatible wallet (private key + address)
2. Saves credentials to .wallet.local.json (gitignored)
- 3. Sets collateral approvals for all Polymarket contracts.
- Collateral + exchange targets flip with POLYMARKET_V2_ENABLED:
- unset/false → USDC.e + V1 exchanges (legacy, retired ~2026-05-05)
- true → pUSD + V2 exchanges (post-cutover 2026-04-28)
+ 3. Sets pUSD + CTF approvals for all Polymarket V2 contracts.
PREREQUISITES:
- - Fund the wallet with POL (for gas) and the active collateral (for trading)
- - V1: USDC.e — if you only have native USDC, run 02_swap_to_usdc_e.py
- - V2: pUSD — if you only have USDC.e, run 02_wrap_to_pusd.py
+ - Fund the wallet with POL (for gas) and pUSD (for trading)
+ - If you only have USDC.e, run 02_wrap_to_pusd.py first
USAGE:
cd backend && uv run python ../experiments/trading/01_setup_wallet.py [command]
@@ -51,46 +47,15 @@
WALLET_PATH = Path(__file__).parent / ".wallet.local.json"
RPC_URL = os.environ["CHAINSTACK_NODE"]
-# Polymarket contracts on Polygon. CTF + NegRisk Adapter unchanged in V2;
-# only the collateral token and the two exchange addresses flip.
+# Polymarket V2 contracts on Polygon (post-2026-04-28 cutover).
CONTRACTS = {
- "USDC_E": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174",
"PUSD": "0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB",
"CTF": "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045",
- "CTF_EXCHANGE": "0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E",
- "CTF_EXCHANGE_V2": "0xE111180000d2663C0091e4f400237545B87B996B",
- "NEG_RISK_CTF_EXCHANGE": "0xC5d563A36AE78145C45a50134d48A1215220f80a",
- "NEG_RISK_CTF_EXCHANGE_V2": "0xe2222d279d744050d28e00520010520000310F59",
- "NEG_RISK_ADAPTER": "0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296",
+ "CTF_EXCHANGE": "0xE111180000d2663C0091e4f400237545B87B996B",
+ "NEG_RISK_CTF_EXCHANGE": "0xe2222d279d744050d28e00520010520000310F59",
}
-def _v2_enabled() -> bool:
- """Match backend's POLYMARKET_V2_ENABLED parsing (experiments are standalone)."""
- return os.environ.get("POLYMARKET_V2_ENABLED", "").strip().lower() in (
- "1",
- "true",
- "yes",
- "on",
- )
-
-
-def _collateral():
- """(label, address) for the active collateral token."""
- return (
- ("pUSD", CONTRACTS["PUSD"])
- if _v2_enabled()
- else ("USDC.e", CONTRACTS["USDC_E"])
- )
-
-
-def _exchanges():
- """(ctf_exchange, neg_risk_exchange) addresses for the active env."""
- if _v2_enabled():
- return CONTRACTS["CTF_EXCHANGE_V2"], CONTRACTS["NEG_RISK_CTF_EXCHANGE_V2"]
- return CONTRACTS["CTF_EXCHANGE"], CONTRACTS["NEG_RISK_CTF_EXCHANGE"]
-
-
ERC20_ABI = [
{
"constant": True,
@@ -194,10 +159,9 @@ def cmd_generate():
print("=" * 60)
print(f"\nAddress: {account.address}")
print(f"Saved to: {WALLET_PATH}")
- coll_label, _ = _collateral()
print("\nNEXT STEPS:")
print(" 1. Send POL to this address (for gas, ~0.5 POL is enough)")
- print(f" 2. Send {coll_label} to this address (for trading)")
+ print(" 2. Send pUSD to this address (for trading)")
print(" 3. Run: uv run python 01_setup_wallet.py approve")
@@ -220,57 +184,45 @@ def cmd_status():
# Balances
pol = w3.from_wei(w3.eth.get_balance(address), "ether")
- usdc = w3.eth.contract(
- address=Web3.to_checksum_address(CONTRACTS["USDC_E"]), abi=ERC20_ABI
+ pusd = w3.eth.contract(
+ address=Web3.to_checksum_address(CONTRACTS["PUSD"]), abi=ERC20_ABI
)
- usdc_balance = usdc.functions.balanceOf(address).call() / 1e6
+ pusd_balance = pusd.functions.balanceOf(address).call() / 1e6
print("\nBalances:")
- print(f" POL: {pol:.4f}")
- print(f" USDC.e: ${usdc_balance:.2f}")
-
- # Under the V2 flag, also show pUSD — V2 collateral after the cutover.
- if _v2_enabled():
- pusd = w3.eth.contract(
- address=Web3.to_checksum_address(CONTRACTS["PUSD"]), abi=ERC20_ABI
- )
- pusd_balance = pusd.functions.balanceOf(address).call() / 1e6
- print(f" pUSD: ${pusd_balance:.2f}")
-
- # Approvals — collateral token + exchange targets flip under V2.
- coll_label, coll_addr = _collateral()
- ctf_exchange, neg_risk_exchange = _exchanges()
- print(f"\nApprovals ({coll_label} collateral):")
- coll = w3.eth.contract(address=Web3.to_checksum_address(coll_addr), abi=ERC20_ABI)
+ print(f" POL: {pol:.4f}")
+ print(f" pUSD: ${pusd_balance:.2f}")
+
+ print("\nApprovals (pUSD collateral):")
ctf = w3.eth.contract(
address=Web3.to_checksum_address(CONTRACTS["CTF"]), abi=CTF_ABI
)
approvals = [
(
- f"{coll_label} → CTF",
- coll.functions.allowance(address, CONTRACTS["CTF"]).call(),
+ "pUSD → CTF",
+ pusd.functions.allowance(address, CONTRACTS["CTF"]).call(),
),
(
- f"{coll_label} → Exchange",
- coll.functions.allowance(address, ctf_exchange).call(),
+ "pUSD → Exchange",
+ pusd.functions.allowance(address, CONTRACTS["CTF_EXCHANGE"]).call(),
),
(
- f"{coll_label} → NegRisk Exchange",
- coll.functions.allowance(address, neg_risk_exchange).call(),
+ "pUSD → NegRisk Exchange",
+ pusd.functions.allowance(
+ address, CONTRACTS["NEG_RISK_CTF_EXCHANGE"]
+ ).call(),
),
(
"CTF → Exchange",
- ctf.functions.isApprovedForAll(address, ctf_exchange).call(),
+ ctf.functions.isApprovedForAll(
+ address, CONTRACTS["CTF_EXCHANGE"]
+ ).call(),
),
(
"CTF → NegRisk Exchange",
- ctf.functions.isApprovedForAll(address, neg_risk_exchange).call(),
- ),
- (
- "CTF → NegRisk Adapter",
ctf.functions.isApprovedForAll(
- address, CONTRACTS["NEG_RISK_ADAPTER"]
+ address, CONTRACTS["NEG_RISK_CTF_EXCHANGE"]
).call(),
),
]
@@ -306,48 +258,40 @@ def cmd_approve():
print(f"ERROR: Insufficient POL for gas (have {pol:.4f}, need ~0.01)")
return
- coll_label, coll_addr = _collateral()
- ctf_exchange, neg_risk_exchange = _exchanges()
-
print("=" * 60)
- print(f"SETTING POLYMARKET APPROVALS ({coll_label} collateral)")
+ print("SETTING POLYMARKET V2 APPROVALS (pUSD collateral)")
print("=" * 60)
- coll = w3.eth.contract(address=Web3.to_checksum_address(coll_addr), abi=ERC20_ABI)
+ pusd = w3.eth.contract(
+ address=Web3.to_checksum_address(CONTRACTS["PUSD"]), abi=ERC20_ABI
+ )
ctf = w3.eth.contract(
address=Web3.to_checksum_address(CONTRACTS["CTF"]), abi=CTF_ABI
)
MAX_UINT256 = 2**256 - 1
approvals = [
- (f"{coll_label} → CTF", coll, "approve", CONTRACTS["CTF"], MAX_UINT256),
- (f"{coll_label} → Exchange", coll, "approve", ctf_exchange, MAX_UINT256),
+ ("pUSD → CTF", pusd, "approve", CONTRACTS["CTF"], MAX_UINT256),
+ ("pUSD → Exchange", pusd, "approve", CONTRACTS["CTF_EXCHANGE"], MAX_UINT256),
(
- f"{coll_label} → NegRisk Exchange",
- coll,
+ "pUSD → NegRisk Exchange",
+ pusd,
"approve",
- neg_risk_exchange,
+ CONTRACTS["NEG_RISK_CTF_EXCHANGE"],
MAX_UINT256,
),
- ("CTF → Exchange", ctf, "setApprovalForAll", ctf_exchange, True),
+ ("CTF → Exchange", ctf, "setApprovalForAll", CONTRACTS["CTF_EXCHANGE"], True),
(
"CTF → NegRisk Exchange",
ctf,
"setApprovalForAll",
- neg_risk_exchange,
- True,
- ),
- (
- "CTF → NegRisk Adapter",
- ctf,
- "setApprovalForAll",
- CONTRACTS["NEG_RISK_ADAPTER"],
+ CONTRACTS["NEG_RISK_CTF_EXCHANGE"],
True,
),
]
for i, (name, contract, method, spender, value) in enumerate(approvals, 1):
- print(f"\n[{i}/6] {name}...")
+ print(f"\n[{i}/{len(approvals)}] {name}...")
try:
fn = getattr(contract.functions, method)
diff --git a/experiments/trading/02_swap_to_usdc_e.py b/experiments/trading/02_swap_to_usdc_e.py
index c028d86..5622abe 100644
--- a/experiments/trading/02_swap_to_usdc_e.py
+++ b/experiments/trading/02_swap_to_usdc_e.py
@@ -20,8 +20,7 @@
for new users and (b) the ParaSwap path itself is independent of
Polymarket's collateral choice.
- V2 cutover timestamp and the CollateralOnramp / pUSD addresses live in
- backend/core/wallet/contracts.py (V2_CONTRACTS).
+ The CollateralOnramp / pUSD addresses live in backend/core/wallet/contracts.py.
WHY THIS IS NEEDED (still):
- Polygon has TWO types of USDC:
diff --git a/experiments/trading/03_buy_position.py b/experiments/trading/03_buy_position.py
index 2a6a68b..665f0ab 100644
--- a/experiments/trading/03_buy_position.py
+++ b/experiments/trading/03_buy_position.py
@@ -66,21 +66,10 @@
RPC_URL = os.environ["CHAINSTACK_NODE"]
CONTRACTS = {
- "USDC_E": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174",
"PUSD": "0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB",
"CTF": "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045",
}
-
-def _v2_enabled() -> bool:
- """Match backend's POLYMARKET_V2_ENABLED parsing (experiments are standalone)."""
- return os.environ.get("POLYMARKET_V2_ENABLED", "").strip().lower() in (
- "1",
- "true",
- "yes",
- "on",
- )
-
ERC20_ABI = [
{
"constant": True,
@@ -168,29 +157,14 @@ async def get_market_info(market_id: str) -> dict:
def get_clob_client():
- """Initialize CLOB client with optional proxy support.
+ """Initialize V2 CLOB client with optional proxy support."""
+ try:
+ from py_clob_client_v2.client import ClobClient
+ import py_clob_client_v2.http_helpers.helpers as clob_helpers
+ except ImportError:
+ print("py-clob-client-v2 not installed. Run: uv add py-clob-client-v2")
+ return None
- Picks V2 (``clob-v2.polymarket.com``) when ``POLYMARKET_V2_ENABLED`` is set;
- otherwise the legacy V1 endpoint. Mirrors backend ``core.trading.clob_client``.
- """
- if _v2_enabled():
- try:
- from py_clob_client_v2.client import ClobClient
- import py_clob_client_v2.http_helpers.helpers as clob_helpers
- except ImportError:
- print("py-clob-client-v2 not installed. Run: uv add py-clob-client-v2")
- return None
- host = "https://clob-v2.polymarket.com"
- else:
- try:
- from py_clob_client.client import ClobClient
- import py_clob_client.http_helpers.helpers as clob_helpers
- except ImportError:
- print("py-clob-client not installed. Run: uv add py-clob-client")
- return None
- host = "https://clob.polymarket.com"
-
- # Proxy support
proxy = os.environ.get("HTTPS_PROXY") or os.environ.get("HTTP_PROXY")
if proxy:
print(f"Using proxy: {proxy[:30]}...")
@@ -199,33 +173,20 @@ def get_clob_client():
wallet = load_wallet()
try:
- if _v2_enabled():
- client = ClobClient(
- host=host,
- chain_id=137,
- key=wallet["private_key"],
- signature_type=0,
- funder=wallet["address"],
- builder_config=None,
- )
- # V2 split V1's `create_or_derive_api_creds` into two calls.
- # Narrow to PolyApiException so transient network errors fail
- # visibly instead of silently creating orphan API keys.
- from py_clob_client_v2.exceptions import PolyApiException
+ client = ClobClient(
+ host="https://clob-v2.polymarket.com",
+ chain_id=137,
+ key=wallet["private_key"],
+ signature_type=0,
+ funder=wallet["address"],
+ builder_config=None,
+ )
+ from py_clob_client_v2.exceptions import PolyApiException
- try:
- creds = client.derive_api_key()
- except PolyApiException:
- creds = client.create_api_key()
- else:
- client = ClobClient(
- host,
- key=wallet["private_key"],
- chain_id=137,
- signature_type=0,
- funder=wallet["address"],
- )
- creds = client.create_or_derive_api_creds()
+ try:
+ creds = client.derive_api_key()
+ except PolyApiException:
+ creds = client.create_api_key()
client.set_api_creds(creds)
return client
except Exception as e:
@@ -325,26 +286,20 @@ async def buy_position(
address = Web3.to_checksum_address(wallet["address"])
account = w3.eth.account.from_key(wallet["private_key"])
- usdc = w3.eth.contract(
- address=Web3.to_checksum_address(CONTRACTS["USDC_E"]), abi=ERC20_ABI
+ pusd = w3.eth.contract(
+ address=Web3.to_checksum_address(CONTRACTS["PUSD"]), abi=ERC20_ABI
)
ctf = w3.eth.contract(
address=Web3.to_checksum_address(CONTRACTS["CTF"]), abi=CTF_ABI
)
- usdc_balance = usdc.functions.balanceOf(address).call()
+ pusd_balance = pusd.functions.balanceOf(address).call()
amount_wei = int(amount * 1e6)
- print(f"\nYour USDC.e: ${usdc_balance / 1e6:.2f}")
- if _v2_enabled():
- pusd = w3.eth.contract(
- address=Web3.to_checksum_address(CONTRACTS["PUSD"]), abi=ERC20_ABI
- )
- pusd_balance = pusd.functions.balanceOf(address).call()
- print(f"Your pUSD: ${pusd_balance / 1e6:.2f}")
+ print(f"\nYour pUSD: ${pusd_balance / 1e6:.2f}")
- if usdc_balance < amount_wei:
- print("ERROR: Insufficient USDC.e")
+ if pusd_balance < amount_wei:
+ print("ERROR: Insufficient pUSD")
return
if not auto_confirm:
@@ -357,11 +312,11 @@ async def buy_position(
# =========================================================================
ctf_address = Web3.to_checksum_address(CONTRACTS["CTF"])
- allowance = usdc.functions.allowance(address, ctf_address).call()
+ allowance = pusd.functions.allowance(address, ctf_address).call()
if allowance < amount_wei:
- print("\n[1/3] Approving USDC.e...")
- tx = usdc.functions.approve(ctf_address, 2**256 - 1).build_transaction(
+ print("\n[1/3] Approving pUSD...")
+ tx = pusd.functions.approve(ctf_address, 2**256 - 1).build_transaction(
{
"from": address,
"nonce": w3.eth.get_transaction_count(address),
@@ -382,10 +337,10 @@ async def buy_position(
# STEP 2: SPLIT
# =========================================================================
- print("\n[2/3] Splitting USDC.e -> YES + NO...")
+ print("\n[2/3] Splitting pUSD -> YES + NO...")
tx = ctf.functions.splitPosition(
- Web3.to_checksum_address(CONTRACTS["USDC_E"]),
+ Web3.to_checksum_address(CONTRACTS["PUSD"]),
bytes(32),
bytes.fromhex(
condition_id[2:] if condition_id.startswith("0x") else condition_id
@@ -426,16 +381,12 @@ async def buy_position(
client = get_clob_client()
if client:
try:
- if _v2_enabled():
- # V2 OrderArgsV2 drops fee_rate_bps/nonce/taker; adds metadata.
- from py_clob_client_v2.clob_types import (
- OrderArgsV2 as OrderArgs,
- OrderType,
- )
- from py_clob_client_v2.order_builder.constants import SELL
- else:
- from py_clob_client.clob_types import OrderArgs, OrderType
- from py_clob_client.order_builder.constants import SELL
+ # V2 OrderArgsV2 drops fee_rate_bps/nonce/taker; adds metadata.
+ from py_clob_client_v2.clob_types import (
+ OrderArgsV2 as OrderArgs,
+ OrderType,
+ )
+ from py_clob_client_v2.order_builder.constants import SELL
# Fetch tick size for correct precision (varies per market)
try:
diff --git a/experiments/trading/04_transfer_tokens.py b/experiments/trading/04_transfer_tokens.py
index 3cc31db..01c0c39 100644
--- a/experiments/trading/04_transfer_tokens.py
+++ b/experiments/trading/04_transfer_tokens.py
@@ -2,10 +2,8 @@
Transfer USDC, USDC.e, and pUSD to Another Wallet
=================================================
-Transfers all stablecoins from the local wallet to a specified address.
-Token set follows POLYMARKET_V2_ENABLED:
- unset/false → native USDC + USDC.e
- true → native USDC + USDC.e + pUSD (V2 collateral)
+Transfers all stablecoins (native USDC, USDC.e, pUSD) from the local wallet
+to a specified address.
USAGE:
cd backend && uv run python ../experiments/trading/04_transfer_tokens.py
@@ -35,15 +33,6 @@
PUSD = "0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB"
-def _v2_enabled() -> bool:
- """Match backend's POLYMARKET_V2_ENABLED parsing (experiments are standalone)."""
- return os.environ.get("POLYMARKET_V2_ENABLED", "").strip().lower() in (
- "1",
- "true",
- "yes",
- "on",
- )
-
ERC20_ABI = [
{
"constant": True,
@@ -114,9 +103,8 @@ def main():
tokens = [
("USDC native", USDC_NATIVE),
("USDC.e", USDC_E),
+ ("pUSD", PUSD),
]
- if _v2_enabled():
- tokens.append(("pUSD", PUSD))
contracts = {
label: w3.eth.contract(address=Web3.to_checksum_address(addr), abi=ERC20_ABI)
diff --git a/frontend/components/PortfolioModal.tsx b/frontend/components/PortfolioModal.tsx
index ce7a23c..b9b4c71 100644
--- a/frontend/components/PortfolioModal.tsx
+++ b/frontend/components/PortfolioModal.tsx
@@ -20,7 +20,7 @@ interface TradeResult {
target: { split_tx?: string; clob_order_id?: string; error?: string }
cover: { split_tx?: string; clob_order_id?: string; error?: string }
total_spent: number
- final_balances: { pol: number; usdc_e: number }
+ final_balances: { pol: number; pusd: number }
warnings?: string[]
}
@@ -111,7 +111,7 @@ export function PortfolioModal({ portfolio: p, onClose }: PortfolioModalProps) {
const MIN_AMOUNT = 5
const amountNum = parseFloat(amount) || 0
const totalCost = amountNum * 2
- const hasSufficientBalance = (status?.balances?.usdc_e || 0) >= totalCost
+ const hasSufficientBalance = (status?.balances?.pusd || 0) >= totalCost
const meetsMinimum = amountNum >= MIN_AMOUNT
const needsUnlock = !walletLoading && !status?.unlocked
@@ -558,7 +558,7 @@ export function PortfolioModal({ portfolio: p, onClose }: PortfolioModalProps) {
- ${(status?.balances?.usdc_e || 0).toFixed(2)} USDC.e
+ ${(status?.balances?.pusd || 0).toFixed(2)} pUSD
@@ -642,7 +642,7 @@ export function PortfolioModal({ portfolio: p, onClose }: PortfolioModalProps) {
Spent ${result.total_spent.toFixed(2)} · Balance: $
- {result.final_balances.usdc_e.toFixed(2)}
+ {result.final_balances.pusd.toFixed(2)}
diff --git a/frontend/components/terminal/WalletDropdown.tsx b/frontend/components/terminal/WalletDropdown.tsx
index e5e7249..363ab3e 100644
--- a/frontend/components/terminal/WalletDropdown.tsx
+++ b/frontend/components/terminal/WalletDropdown.tsx
@@ -136,7 +136,7 @@ export function WalletDropdown() {
// Determine button state
const isUnlocked = status?.unlocked
const hasWallet = status?.exists
- const balance = status?.balances?.usdc_e ?? 0
+ const balance = status?.balances?.pusd ?? 0
return (
@@ -446,9 +446,9 @@ export function WalletDropdown() {
{/* Balances */}
- USDC.e
+ pUSD
- ${(status?.balances?.usdc_e ?? 0).toFixed(2)}
+ ${(status?.balances?.pusd ?? 0).toFixed(2)}
diff --git a/frontend/hooks/useWallet.tsx b/frontend/hooks/useWallet.tsx
index a2420f9..6a93459 100644
--- a/frontend/hooks/useWallet.tsx
+++ b/frontend/hooks/useWallet.tsx
@@ -12,7 +12,7 @@ import { getApiBaseUrl } from '@/config/api-config'
interface WalletBalances {
pol: number
- usdc_e: number
+ pusd: number
}
interface WalletStatus {