Skip to content

Commit 142d44e

Browse files
v15.1.3 (#54)
1 parent 0d534fc commit 142d44e

File tree

6 files changed

+220
-16
lines changed

6 files changed

+220
-16
lines changed

.coverage

0 Bytes
Binary file not shown.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "sakit"
3-
version = "15.1.2"
3+
version = "15.1.3"
44
description = "Solana Agent Kit"
55
authors = ["Bevan Hunt <bevan@bevanhunt.com>"]
66
license = "MIT"

sakit/dflow_prediction.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,7 @@ async def sign_and_send(tx_b64: str) -> str:
444444
"amount_in": f"{amount} USDC",
445445
"tokens_received": result.out_amount,
446446
"signature": result.signature,
447+
"tx_signature": result.signature,
447448
"execution_mode": result.execution_mode,
448449
"safety": safety,
449450
}
@@ -524,6 +525,7 @@ async def sign_and_send(tx_b64: str) -> str:
524525
"tokens_sold": f"{amount} {side.upper()}",
525526
"usdc_received": result.out_amount,
526527
"signature": result.signature,
528+
"tx_signature": result.signature,
527529
"execution_mode": result.execution_mode,
528530
}
529531
else:

sakit/privy_dflow_prediction.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -701,6 +701,7 @@ async def sign_and_send(tx_b64: str) -> str: # pragma: no cover
701701
"amount_in": f"{amount} USDC",
702702
"tokens_received": result.out_amount,
703703
"signature": result.signature,
704+
"tx_signature": result.signature,
704705
"execution_mode": result.execution_mode,
705706
"wallet": wallet_address,
706707
"safety": safety,
@@ -813,6 +814,7 @@ async def sign_and_send(tx_b64: str) -> str: # pragma: no cover
813814
"tokens_sold": f"{amount} {side.upper()}",
814815
"usdc_received": result.out_amount,
815816
"signature": result.signature,
817+
"tx_signature": result.signature,
816818
"execution_mode": result.execution_mode,
817819
"wallet": wallet_address,
818820
}

sakit/utils/dflow.py

Lines changed: 84 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
DFLOW_METADATA_API = "https://prediction-markets-api.dflow.net/api/v1"
3535

3636
# Known verified series for safety scoring
37+
# These are well-established markets with clear resolution criteria
3738
KNOWN_SERIES = {
3839
"US-POLITICS",
3940
"US-ELECTIONS",
@@ -47,6 +48,22 @@
4748
"ECONOMICS",
4849
}
4950

51+
# Series prefixes that indicate verified/safe markets (e.g., Kalshi markets)
52+
SAFE_SERIES_PREFIXES = {
53+
"KX", # Kalshi Exchange - regulated, clear resolution
54+
"POLY", # Polymarket - established platform
55+
}
56+
57+
# Categories that have objectively verifiable outcomes (binary, date-bound)
58+
OBJECTIVE_CATEGORIES = {
59+
"politics",
60+
"elections",
61+
"sports",
62+
"fed",
63+
"economics",
64+
"crypto-price",
65+
}
66+
5067
# Default quality filters
5168
DEFAULT_MIN_VOLUME_USD = 1000
5269
DEFAULT_MIN_LIQUIDITY_USD = 500
@@ -424,16 +441,49 @@ def calculate_safety_score(
424441
score_points = 100
425442
now = current_time or int(time.time())
426443

427-
# Age check
444+
# Get market identifiers
445+
ticker = market.get("ticker", "") or ""
446+
series_ticker = market.get("seriesTicker") or market.get("series_ticker", "") or ""
447+
category = (market.get("category", "") or "").lower()
448+
449+
# Check if this is a verified/regulated market (Kalshi, Polymarket, etc.)
450+
is_verified_platform = any(
451+
ticker.upper().startswith(prefix) or series_ticker.upper().startswith(prefix)
452+
for prefix in SAFE_SERIES_PREFIXES
453+
)
454+
455+
# Check if this is a known established series
456+
is_known_series = any(
457+
series_ticker.upper().startswith(known) for known in KNOWN_SERIES
458+
)
459+
460+
# Check if the market has objectively verifiable outcomes
461+
has_clear_resolution_date = bool(
462+
market.get("closeTime") or market.get("expirationTime") or market.get("endDate")
463+
)
464+
is_objective_category = category in OBJECTIVE_CATEGORIES
465+
466+
# Verified platforms with clear dates are HIGH safety - skip other checks
467+
if is_verified_platform and has_clear_resolution_date:
468+
# These are regulated markets with binary outcomes and set dates
469+
return SafetyResult("HIGH", [], "PROCEED")
470+
471+
# Known series with clear dates also get a boost
472+
if is_known_series and has_clear_resolution_date:
473+
return SafetyResult("HIGH", [], "PROCEED")
474+
475+
# Age check - less strict for verified platforms
428476
created_at = market.get("createdAt") or market.get("openTime")
429477
if created_at:
430478
age_hours = (now - created_at) / 3600
431479
if age_hours < 24:
432-
warnings.append("New market (< 24 hours old)")
433-
score_points -= 30
480+
if not is_verified_platform:
481+
warnings.append("New market (< 24 hours old)")
482+
score_points -= 30
434483
elif age_hours < 168: # 7 days
435-
warnings.append("Young market (< 7 days old)")
436-
score_points -= 15
484+
if not is_verified_platform:
485+
warnings.append("Young market (< 7 days old)")
486+
score_points -= 15
437487

438488
# Volume check
439489
volume = market.get("volume", 0)
@@ -461,22 +511,21 @@ def calculate_safety_score(
461511
warnings.append("No trades in 24 hours")
462512
score_points -= 20
463513

464-
# Series verification
465-
series_ticker = market.get("seriesTicker") or market.get("series_ticker", "")
466-
# Check if any known series is a prefix of the series ticker
467-
is_known_series = any(
468-
series_ticker.upper().startswith(known) for known in KNOWN_SERIES
469-
)
470-
if not is_known_series and series_ticker:
514+
# Series verification - only penalize if not verified platform
515+
if not is_known_series and not is_verified_platform and series_ticker:
471516
warnings.append("Unknown/unverified series")
472517
score_points -= 15
473518

474-
# Resolution clarity check
519+
# Resolution clarity check - verified platforms have clear rules by default
475520
rules = market.get("rulesPrimary", "")
476-
if not rules or len(rules) < 50:
521+
if not is_verified_platform and (not rules or len(rules) < 50):
477522
warnings.append("Unclear resolution criteria")
478523
score_points -= 20
479524

525+
# Boost for objective categories with clear dates
526+
if is_objective_category and has_clear_resolution_date:
527+
score_points += 15
528+
480529
# Calculate final score
481530
if score_points >= 70:
482531
return SafetyResult("HIGH", warnings, "PROCEED")
@@ -547,12 +596,32 @@ def _add_safety_scores(
547596
items: List[Dict[str, Any]],
548597
trades_by_ticker: Optional[Dict[str, List[Dict[str, Any]]]] = None,
549598
) -> List[Dict[str, Any]]:
550-
"""Add safety scores to markets/events."""
599+
"""Add safety scores and formatted resolution dates to markets/events."""
600+
from datetime import datetime
601+
551602
for item in items:
552603
ticker = item.get("ticker", "")
553604
trades = trades_by_ticker.get(ticker) if trades_by_ticker else None
554605
safety = calculate_safety_score(item, trades)
555606
item["safety"] = safety.to_dict()
607+
608+
# Add human-readable resolution date
609+
close_time = (
610+
item.get("closeTime")
611+
or item.get("expirationTime")
612+
or item.get("endDate")
613+
)
614+
if close_time:
615+
try:
616+
if isinstance(close_time, (int, float)):
617+
from datetime import timezone
618+
619+
dt = datetime.fromtimestamp(close_time, tz=timezone.utc)
620+
item["resolution_date"] = dt.strftime("%Y-%m-%d %H:%M UTC")
621+
else:
622+
item["resolution_date"] = str(close_time)
623+
except Exception:
624+
item["resolution_date"] = str(close_time)
556625
return items
557626

558627
# =========================================================================

tests/test_dflow_prediction_tool.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,64 @@ def test_medium_safety_score(self):
258258
assert result.score == "MEDIUM"
259259
assert result.recommendation == "CAUTION"
260260

261+
def test_kalshi_market_with_clear_date_gets_high_score(self):
262+
"""Kalshi markets (KX prefix) with clear resolution date should get HIGH score."""
263+
market = {
264+
"ticker": "KXTRUMPOUT-26-TRUMP",
265+
"volume": 500, # Would normally be low
266+
"liquidity": 100, # Would normally be low
267+
"closeTime": int(time.time()) + 86400 * 30, # 30 days from now
268+
"rulesPrimary": "short", # Would normally trigger warning
269+
}
270+
result = calculate_safety_score(market)
271+
assert result.score == "HIGH"
272+
assert result.recommendation == "PROCEED"
273+
assert len(result.warnings) == 0
274+
275+
def test_polymarket_with_clear_date_gets_high_score(self):
276+
"""Polymarket (POLY prefix) with clear resolution date should get HIGH score."""
277+
market = {
278+
"ticker": "POLY-ELECTION-2024",
279+
"volume": 500,
280+
"liquidity": 100,
281+
"expirationTime": int(time.time()) + 86400 * 30,
282+
"rulesPrimary": "short",
283+
}
284+
result = calculate_safety_score(market)
285+
assert result.score == "HIGH"
286+
assert result.recommendation == "PROCEED"
287+
288+
def test_verified_platform_without_date_still_checks_other_factors(self):
289+
"""Verified platform without clear date should still check other factors."""
290+
market = {
291+
"ticker": "KXSOMETHING",
292+
"volume": 500, # Low volume
293+
"liquidity": 100, # Low liquidity
294+
# No closeTime or expirationTime
295+
"rulesPrimary": "short",
296+
}
297+
result = calculate_safety_score(market)
298+
# Without clear date, should not auto-pass
299+
assert result.score != "HIGH" or len(result.warnings) > 0
300+
301+
def test_objective_category_with_date_gets_boost(self):
302+
"""Objective category (e.g., politics) with clear date should get score boost."""
303+
# This market would normally be MEDIUM but gets boosted
304+
market = {
305+
"ticker": "SOMEMARKET",
306+
"category": "politics",
307+
"volume": 5000, # Moderate volume (-10)
308+
"liquidity": 1500, # Moderate liquidity (-10)
309+
"closeTime": int(time.time()) + 86400 * 30, # Has clear date
310+
"rulesPrimary": "x" * 100, # Clear rules
311+
"seriesTicker": "UNKNOWN-SERIES", # Unknown series (-15)
312+
# Without boost: 100 - 10 - 10 - 15 = 65 (MEDIUM)
313+
# With boost: 65 + 15 = 80 (HIGH)
314+
}
315+
result = calculate_safety_score(market)
316+
assert result.score == "HIGH"
317+
assert result.recommendation == "PROCEED"
318+
261319

262320
# =============================================================================
263321
# TOOL SCHEMA TESTS
@@ -899,6 +957,79 @@ def test_add_safety_scores(self):
899957
assert result[0]["safety"]["score"] in ["HIGH", "MEDIUM", "LOW"]
900958
assert "safety" in result[1]
901959

960+
def test_add_safety_scores_with_resolution_date_int(self):
961+
"""Resolution date should be formatted from int timestamp."""
962+
client = DFlowPredictionClient()
963+
964+
items = [
965+
{
966+
"ticker": "A",
967+
"volume": 50000,
968+
"liquidity": 10000,
969+
"rulesPrimary": "x" * 100,
970+
"closeTime": 1735689600, # 2025-01-01 00:00:00 UTC
971+
},
972+
]
973+
974+
result = client._add_safety_scores(items)
975+
assert "resolution_date" in result[0]
976+
assert "2025-01-01" in result[0]["resolution_date"]
977+
assert "UTC" in result[0]["resolution_date"]
978+
979+
def test_add_safety_scores_with_resolution_date_string(self):
980+
"""Resolution date should handle string date."""
981+
client = DFlowPredictionClient()
982+
983+
items = [
984+
{
985+
"ticker": "A",
986+
"volume": 50000,
987+
"liquidity": 10000,
988+
"rulesPrimary": "x" * 100,
989+
"closeTime": "2025-01-01T00:00:00Z", # String format
990+
},
991+
]
992+
993+
result = client._add_safety_scores(items)
994+
assert "resolution_date" in result[0]
995+
assert result[0]["resolution_date"] == "2025-01-01T00:00:00Z"
996+
997+
def test_add_safety_scores_no_resolution_date(self):
998+
"""Items without resolution date should not have resolution_date field."""
999+
client = DFlowPredictionClient()
1000+
1001+
items = [
1002+
{
1003+
"ticker": "A",
1004+
"volume": 50000,
1005+
"liquidity": 10000,
1006+
"rulesPrimary": "x" * 100,
1007+
# No closeTime, expirationTime, or endDate
1008+
},
1009+
]
1010+
1011+
result = client._add_safety_scores(items)
1012+
assert "resolution_date" not in result[0]
1013+
1014+
def test_add_safety_scores_with_invalid_timestamp(self):
1015+
"""Invalid timestamp should fallback to string representation."""
1016+
client = DFlowPredictionClient()
1017+
1018+
items = [
1019+
{
1020+
"ticker": "A",
1021+
"volume": 50000,
1022+
"liquidity": 10000,
1023+
"rulesPrimary": "x" * 100,
1024+
"closeTime": -99999999999999, # Invalid timestamp
1025+
},
1026+
]
1027+
1028+
result = client._add_safety_scores(items)
1029+
assert "resolution_date" in result[0]
1030+
# Should fallback to string of the invalid value
1031+
assert "-99999999999999" in result[0]["resolution_date"]
1032+
9021033

9031034
# =============================================================================
9041035
# BLOCKING EXECUTION TESTS

0 commit comments

Comments
 (0)