Skip to content

Commit d53388b

Browse files
authored
Merge pull request #37 from kaizencycle/cursor/mic-ledger-and-wallet-sync-6a0b
Mic ledger and wallet sync
2 parents a5b6d1c + 2da42ae commit d53388b

12 files changed

Lines changed: 535 additions & 7 deletions
0 Bytes
Binary file not shown.
4.48 KB
Binary file not shown.

app/main.py

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,15 @@
2020
RewardEstimate,
2121
SessionStatus,
2222
CircuitBreakerStatus,
23+
# MIC Wallet schemas
24+
MICReason,
25+
MICLedgerEntry,
26+
WalletBalanceResponse,
27+
WalletLedgerResponse,
2328
)
2429
from app.services.learning_store import learning_store
2530
from app.services.mic_minting import MICMintingService
31+
from app.services.mic_ledger_store import mic_ledger_store
2632

2733
# Initialize services
2834
mic_service = MICMintingService()
@@ -1074,6 +1080,9 @@ async def complete_learning_session(session_id: str, req: SessionCompleteRequest
10741080
is_first_module=updated_progress["modules_completed"] == 1
10751081
)
10761082

1083+
# Get new wallet balance from ledger (DERIVED, never stored)
1084+
new_wallet_balance = mic_ledger_store.get_balance(user_id)
1085+
10771086
return SessionCompleteResponse(
10781087
session_id=session_id,
10791088
module_id=module_id,
@@ -1083,6 +1092,8 @@ async def complete_learning_session(session_id: str, req: SessionCompleteRequest
10831092
new_level=updated_progress["level"],
10841093
integrity_score=updated_progress.get("integrity_score", 0.85),
10851094
transaction_id=transaction_id,
1095+
ledger_id=mint_result.get("ledger_id"), # Proof of earning
1096+
new_wallet_balance=new_wallet_balance, # Derived from ledger
10861097
status=SessionStatus.COMPLETED,
10871098
rewards={
10881099
"mic": mic_earned,
@@ -1189,6 +1200,146 @@ def get_learning_system_status():
11891200
}
11901201

11911202

1203+
# =============================================================================
1204+
# MIC WALLET API ENDPOINTS (C-151 MIC Ledger & Wallet Sync)
1205+
# =============================================================================
1206+
1207+
@app.get("/api/v1/wallet/balance")
1208+
def get_wallet_balance(user_id: str):
1209+
"""
1210+
Get user's MIC wallet balance.
1211+
1212+
CRITICAL: Balance is DERIVED from the ledger, never stored separately.
1213+
This ensures integrity and auditability of all MIC transactions.
1214+
1215+
Query Parameters:
1216+
- user_id: User ID to get balance for
1217+
1218+
Returns:
1219+
- balance: Total MIC balance (sum of all ledger entries)
1220+
- last_updated: Timestamp of last transaction
1221+
- recent_events: Last 10 ledger entries
1222+
"""
1223+
# Get derived balance from ledger
1224+
balance = mic_ledger_store.get_balance(user_id)
1225+
1226+
# Get recent entries
1227+
recent_entries = mic_ledger_store.get_recent_entries(user_id, limit=10)
1228+
1229+
# Get last entry for timestamp
1230+
last_entry = mic_ledger_store.get_last_entry(user_id)
1231+
last_updated = last_entry.created_at if last_entry else None
1232+
1233+
# Format recent events for response
1234+
recent_events = [
1235+
{
1236+
"id": entry.id,
1237+
"amount": entry.amount,
1238+
"reason": entry.reason.value,
1239+
"module_id": entry.module_id,
1240+
"integrity_score": entry.integrity_score,
1241+
"timestamp": entry.created_at.isoformat()
1242+
}
1243+
for entry in recent_entries
1244+
]
1245+
1246+
return WalletBalanceResponse(
1247+
user_id=user_id,
1248+
balance=balance,
1249+
last_updated=last_updated,
1250+
recent_events=recent_events
1251+
)
1252+
1253+
1254+
@app.get("/api/v1/wallet/ledger")
1255+
def get_wallet_ledger(
1256+
user_id: str,
1257+
limit: int = 50,
1258+
offset: int = 0
1259+
):
1260+
"""
1261+
Get full MIC ledger history for a user.
1262+
1263+
The ledger is append-only and auditable - entries are never modified.
1264+
1265+
Query Parameters:
1266+
- user_id: User ID to get ledger for
1267+
- limit: Number of entries to return (default 50, max 100)
1268+
- offset: Pagination offset
1269+
1270+
Returns:
1271+
- total_entries: Total number of ledger entries
1272+
- entries: List of ledger entries (most recent first)
1273+
"""
1274+
# Cap limit at 100
1275+
limit = min(limit, 100)
1276+
1277+
total, entries = mic_ledger_store.get_ledger(user_id, limit=limit, offset=offset)
1278+
1279+
return WalletLedgerResponse(
1280+
user_id=user_id,
1281+
total_entries=total,
1282+
entries=entries
1283+
)
1284+
1285+
1286+
@app.get("/api/v1/wallet/breakdown")
1287+
def get_wallet_breakdown(user_id: str):
1288+
"""
1289+
Get MIC balance breakdown by transaction type.
1290+
1291+
Shows how much MIC was earned through each channel:
1292+
- LEARN: Learning module completions
1293+
- EARN: Other earning activities
1294+
- BONUS: Streak and achievement bonuses
1295+
- CORRECTION: Manual adjustments
1296+
1297+
Query Parameters:
1298+
- user_id: User ID to get breakdown for
1299+
1300+
Returns:
1301+
- Breakdown by reason type
1302+
- Total balance
1303+
"""
1304+
breakdown = mic_ledger_store.get_balance_breakdown(user_id)
1305+
1306+
return {
1307+
"user_id": user_id,
1308+
"breakdown": breakdown,
1309+
"balance": breakdown.get("total", 0.0)
1310+
}
1311+
1312+
1313+
@app.options("/api/v1/wallet/balance")
1314+
def wallet_balance_options():
1315+
"""CORS preflight for wallet balance endpoint."""
1316+
return JSONResponse(
1317+
content={},
1318+
status_code=204,
1319+
headers={
1320+
"Access-Control-Allow-Origin": "*",
1321+
"Access-Control-Allow-Methods": "GET, OPTIONS",
1322+
"Access-Control-Allow-Headers": "Content-Type, Authorization, X-Requested-With",
1323+
"Access-Control-Max-Age": "86400"
1324+
}
1325+
)
1326+
1327+
1328+
@app.options("/api/v1/wallet/ledger")
1329+
def wallet_ledger_options():
1330+
"""CORS preflight for wallet ledger endpoint."""
1331+
return JSONResponse(
1332+
content={},
1333+
status_code=204,
1334+
headers={
1335+
"Access-Control-Allow-Origin": "*",
1336+
"Access-Control-Allow-Methods": "GET, OPTIONS",
1337+
"Access-Control-Allow-Headers": "Content-Type, Authorization, X-Requested-With",
1338+
"Access-Control-Max-Age": "86400"
1339+
}
1340+
)
1341+
1342+
11921343
# =============================================================================
11931344
# ROOT ENDPOINT
11941345
# =============================================================================
@@ -1221,6 +1372,9 @@ def root():
12211372
"learning_progress": {"path": "/api/learning/users/{id}/progress", "method": "GET", "description": "Get user progress"},
12221373
"learning_estimate": {"path": "/api/learning/estimate-reward", "method": "GET", "description": "Estimate MIC reward"},
12231374
"learning_status": {"path": "/api/learning/system-status", "method": "GET", "description": "System & circuit breaker status"},
1375+
"wallet_balance": {"path": "/api/v1/wallet/balance", "method": "GET", "description": "Get MIC wallet balance (derived from ledger)"},
1376+
"wallet_ledger": {"path": "/api/v1/wallet/ledger", "method": "GET", "description": "Get full MIC transaction history"},
1377+
"wallet_breakdown": {"path": "/api/v1/wallet/breakdown", "method": "GET", "description": "Get MIC balance breakdown by type"},
12241378
"debug_anthropic": {"path": "/api/debug/test-anthropic", "method": "GET"},
12251379
"debug_openai": {"path": "/api/debug/test-openai", "method": "GET"},
12261380
"agents_register": {"path": "/agents/register", "method": "POST"},
0 Bytes
Binary file not shown.
3.3 KB
Binary file not shown.

app/models/learning.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,57 @@ class CircuitBreakerStatus(str, Enum):
2929
CIRCUIT_BREAKER_ACTIVE = "circuit_breaker_active"
3030

3131

32+
# =============================================================================
33+
# MIC LEDGER SCHEMAS (Append-only ledger for wallet tracking)
34+
# =============================================================================
35+
36+
class MICReason(str, Enum):
37+
"""Reason for MIC transaction - determines source of MIC credit"""
38+
LEARN = "LEARN" # Earned through learning module completion
39+
EARN = "EARN" # Earned through other activities
40+
CORRECTION = "CORRECTION" # Manual correction (can be positive or negative)
41+
BONUS = "BONUS" # Bonus reward (streak, achievement, etc.)
42+
43+
44+
class MICLedgerEntry(BaseModel):
45+
"""Single entry in the append-only MIC ledger"""
46+
id: str = Field(..., description="Unique ledger entry ID")
47+
user_id: str = Field(..., description="User who earned/spent the MIC")
48+
amount: float = Field(..., description="MIC amount (positive for credit, negative for debit)")
49+
reason: MICReason = Field(..., description="Reason for the transaction")
50+
integrity_score: float = Field(..., ge=0.0, le=1.0, description="User's integrity score at time of transaction")
51+
gii: Optional[float] = Field(None, ge=0.0, le=1.0, description="Global Integrity Index at time of transaction")
52+
module_id: Optional[str] = Field(None, description="Associated learning module (if LEARN reason)")
53+
session_id: Optional[str] = Field(None, description="Associated session (if applicable)")
54+
transaction_id: Optional[str] = Field(None, description="External transaction ID")
55+
metadata: Optional[Dict[str, Any]] = Field(None, description="Additional context")
56+
created_at: datetime = Field(default_factory=datetime.utcnow, description="Timestamp of transaction")
57+
58+
class Config:
59+
from_attributes = True
60+
61+
62+
class WalletBalanceResponse(BaseModel):
63+
"""Response for wallet balance query - balance is DERIVED from ledger"""
64+
user_id: str
65+
balance: float = Field(..., description="Total MIC balance (sum of all ledger entries)")
66+
last_updated: Optional[datetime] = Field(None, description="Timestamp of last transaction")
67+
recent_events: List[Dict[str, Any]] = Field(default_factory=list, description="Recent ledger entries")
68+
69+
class Config:
70+
from_attributes = True
71+
72+
73+
class WalletLedgerResponse(BaseModel):
74+
"""Response for full ledger query - append-only, auditable"""
75+
user_id: str
76+
total_entries: int
77+
entries: List[MICLedgerEntry]
78+
79+
class Config:
80+
from_attributes = True
81+
82+
3283
# Question Schema
3384
# ================
3485

@@ -167,6 +218,8 @@ class SessionCompleteResponse(BaseModel):
167218
new_level: int
168219
integrity_score: float
169220
transaction_id: Optional[str] = None
221+
ledger_id: Optional[str] = None # MIC Ledger entry ID (proof of earning)
222+
new_wallet_balance: Optional[float] = None # Updated wallet balance from ledger
170223
status: SessionStatus
171224
rewards: Dict[str, int]
172225
bonuses: Dict[str, float]
0 Bytes
Binary file not shown.
0 Bytes
Binary file not shown.
9.81 KB
Binary file not shown.
917 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)