@@ -1226,10 +1226,111 @@ def preflight_next_step_label(action: str) -> str:
12261226 "MANUAL_REVIEW" : "Escalate for manual review" ,
12271227 "DO_NOT_SEND" : "Do not send" ,
12281228 "REVIEW_REQUEST_TEMPLATE" : "Correct the approved payout instructions" ,
1229+ "REVERIFY_DESTINATION" : "Re-verify the destination route" ,
1230+ "CONFIRM_DESTINATION" : "Confirm the destination before approval" ,
12291231 }
12301232 return labels .get (str (action or "" ).upper (), str (action or "" ).replace ("_" , " " ).title () or "Review required" )
12311233
12321234
1235+ POLICY_PROFILE_ALIASES = {
1236+ "" : "standard" ,
1237+ "default" : "standard" ,
1238+ "standard" : "standard" ,
1239+ "payout_strict" : "payout_strict" ,
1240+ "strict" : "payout_strict" ,
1241+ "deposit_review" : "deposit_review" ,
1242+ "deposit" : "deposit_review" ,
1243+ "treasury_review" : "treasury_review" ,
1244+ "treasury" : "treasury_review" ,
1245+ }
1246+
1247+
1248+ def normalize_policy_profile (value : Any ) -> str :
1249+ key = str (value or "" ).strip ().lower ()
1250+ return POLICY_PROFILE_ALIASES .get (key , "standard" )
1251+
1252+
1253+ def policy_profile_label (profile : str ) -> str :
1254+ labels = {
1255+ "standard" : "Standard" ,
1256+ "payout_strict" : "Payout Strict" ,
1257+ "deposit_review" : "Deposit Review" ,
1258+ "treasury_review" : "Treasury Review" ,
1259+ }
1260+ return labels .get (normalize_policy_profile (profile ), "Standard" )
1261+
1262+
1263+ def derive_preflight_policy_override (destination_class : str , policy_profile : str ) -> Optional [Dict [str , str ]]:
1264+ profile = normalize_policy_profile (policy_profile )
1265+ destination_class = str (destination_class or "" ).lower ()
1266+ if profile == "standard" or destination_class in {"" , "personal_wallet" , "unavailable" }:
1267+ return None
1268+
1269+ reason_map = {
1270+ "contract_or_app" : "DESTINATION_IS_CONTRACT_OR_APP" ,
1271+ "bridge_router" : "DESTINATION_IS_BRIDGE_ROUTER" ,
1272+ "exchange_like_deposit" : "DESTINATION_REQUIRES_MEMO_OR_VENUE_CHECK" ,
1273+ "unknown" : "DESTINATION_NOT_CLASSIFIED" ,
1274+ "not_found" : "DESTINATION_NOT_CLASSIFIED" ,
1275+ }
1276+ summary_map = {
1277+ "contract_or_app" : "The destination looks like a contract or app rather than a simple wallet route." ,
1278+ "bridge_router" : "The destination looks like bridge or router infrastructure rather than a simple wallet route." ,
1279+ "exchange_like_deposit" : "The destination looks like an exchange-style or venue-dependent deposit route." ,
1280+ "unknown" : "The destination could not be classified confidently." ,
1281+ "not_found" : "The destination could not be classified confidently." ,
1282+ }
1283+ reason_code = reason_map .get (destination_class )
1284+ summary = summary_map .get (destination_class )
1285+ if not reason_code or not summary :
1286+ return None
1287+
1288+ if profile == "payout_strict" :
1289+ return {
1290+ "status" : "blocked" ,
1291+ "verdict" : "BLOCK" ,
1292+ "reason_code" : reason_code ,
1293+ "next_action" : "BLOCK_AND_REVERIFY" ,
1294+ "confidence" : "High" ,
1295+ "summary" : summary ,
1296+ "why" : "Payout Strict only approves clear personal-wallet style destinations. Ambiguous, infrastructure, or venue-dependent routes should be stopped and re-verified before funds move." ,
1297+ }
1298+
1299+ if profile == "deposit_review" :
1300+ return {
1301+ "status" : "review_required" ,
1302+ "verdict" : "REVERIFY" ,
1303+ "reason_code" : reason_code ,
1304+ "next_action" : "REVERIFY_DESTINATION" if destination_class != "exchange_like_deposit" else "RECHECK_MEMO_OR_TAG" ,
1305+ "confidence" : "Medium" ,
1306+ "summary" : summary ,
1307+ "why" : "Deposit Review keeps ambiguous or venue-dependent routes in a confirmation path instead of treating them like normal wallet payouts." ,
1308+ }
1309+
1310+ if profile == "treasury_review" :
1311+ if destination_class in {"exchange_like_deposit" , "bridge_router" }:
1312+ return {
1313+ "status" : "blocked" ,
1314+ "verdict" : "BLOCK" ,
1315+ "reason_code" : reason_code ,
1316+ "next_action" : "BLOCK_AND_REVERIFY" ,
1317+ "confidence" : "High" ,
1318+ "summary" : summary ,
1319+ "why" : "Treasury Review blocks routes that look like infrastructure or venue-dependent deposit paths until the destination is re-confirmed through an approved internal process." ,
1320+ }
1321+ return {
1322+ "status" : "review_required" ,
1323+ "verdict" : "REVERIFY" ,
1324+ "reason_code" : reason_code ,
1325+ "next_action" : "REVERIFY_DESTINATION" ,
1326+ "confidence" : "Medium" ,
1327+ "summary" : summary ,
1328+ "why" : "Treasury Review keeps ambiguous destinations in a cautious review path before release." ,
1329+ }
1330+
1331+ return None
1332+
1333+
12331334def derive_preflight_outcome (
12341335 * ,
12351336 checks : Dict [str , bool ],
@@ -1243,6 +1344,7 @@ def derive_preflight_outcome(
12431344 provided_valid : bool ,
12441345 provided_destination : Dict [str , Any ],
12451346 risk_flags : List [str ],
1347+ policy_profile : str = "standard" ,
12461348) -> Dict [str , str ]:
12471349 mismatch_reason = None
12481350 for code , is_failed in [
@@ -1319,6 +1421,22 @@ def derive_preflight_outcome(
13191421 }
13201422
13211423 destination_class = provided_destination .get ("classification" )
1424+
1425+ if provided_destination .get ("source" ) == "unavailable" :
1426+ return {
1427+ "status" : "unavailable" ,
1428+ "verdict" : "UNAVAILABLE" ,
1429+ "reason_code" : "DESTINATION_LOOKUP_UNAVAILABLE" ,
1430+ "next_action" : "CHECK_BACKEND" ,
1431+ "confidence" : "Limited" ,
1432+ "summary" : "Live destination lookup is currently unavailable." ,
1433+ "why" : "The service could compare the transfer fields, but the live destination classification step did not complete. Retry or review manually." ,
1434+ }
1435+
1436+ profile_override = derive_preflight_policy_override (destination_class , policy_profile )
1437+ if profile_override :
1438+ return profile_override
1439+
13221440 if destination_class == "bridge_router" :
13231441 return {
13241442 "status" : "review_required" ,
@@ -1352,17 +1470,6 @@ def derive_preflight_outcome(
13521470 "why" : "Deposit-style destinations often depend on the exact venue, network, asset, and memo or tag. Re-verify all of them before sending." ,
13531471 }
13541472
1355- if provided_destination .get ("source" ) == "unavailable" :
1356- return {
1357- "status" : "unavailable" ,
1358- "verdict" : "UNAVAILABLE" ,
1359- "reason_code" : "DESTINATION_LOOKUP_UNAVAILABLE" ,
1360- "next_action" : "CHECK_BACKEND" ,
1361- "confidence" : "Limited" ,
1362- "summary" : "Live destination lookup is currently unavailable." ,
1363- "why" : "The service could compare the transfer fields, but the live destination classification step did not complete. Retry or review manually." ,
1364- }
1365-
13661473 if destination_class in {"unknown" , "not_found" }:
13671474 return {
13681475 "status" : "review_required" ,
@@ -3805,6 +3912,8 @@ def preflight_check():
38053912 payload = request .get_json (silent = True ) or {}
38063913 expected = payload .get ("expected" ) or {}
38073914 provided = payload .get ("provided" ) or {}
3915+ context = payload .get ("context" ) or {}
3916+ policy_profile = normalize_policy_profile (payload .get ("policy_profile" ) or context .get ("policy_profile" ))
38083917
38093918 expected_chain = normalize_chain (expected .get ("network" ) or expected .get ("chain" ))
38103919 provided_chain = normalize_chain (provided .get ("network" ) or provided .get ("chain" ))
@@ -3924,6 +4033,7 @@ def preflight_check():
39244033 provided_valid = provided_valid ,
39254034 provided_destination = provided_destination ,
39264035 risk_flags = risk_flags ,
4036+ policy_profile = policy_profile ,
39274037 )
39284038
39294039 checked_at = utc_now_iso ()
@@ -3940,6 +4050,7 @@ def preflight_check():
39404050 "verdict" : outcome ["verdict" ],
39414051 "next_action" : outcome ["next_action" ],
39424052 "risk_flags" : sorted (set (risk_flags )),
4053+ "policy_profile" : policy_profile ,
39434054 },
39444055 )
39454056 response_payload = {
@@ -3949,6 +4060,8 @@ def preflight_check():
39494060 "trace_id" : current_request_id (),
39504061 "checked_at" : checked_at ,
39514062 "chain" : provided_chain ,
4063+ "policy_profile" : policy_profile ,
4064+ "policy_profile_label" : policy_profile_label (policy_profile ),
39524065 "status" : outcome ["status" ],
39534066 "verdict" : outcome ["verdict" ],
39544067 "reason_code" : outcome ["reason_code" ],
@@ -3969,6 +4082,7 @@ def preflight_check():
39694082 "reason_code" : outcome ["reason_code" ],
39704083 "next_action" : outcome ["next_action" ],
39714084 "next_action_label" : preflight_next_step_label (outcome ["next_action" ]),
4085+ "policy_profile" : policy_profile ,
39724086 },
39734087 "expected" : {
39744088 "network" : expected_chain ,
@@ -4024,6 +4138,7 @@ def preflight_check():
40244138 metadata = {
40254139 "next_action" : outcome ["next_action" ],
40264140 "confidence" : outcome ["confidence" ],
4141+ "policy_profile" : policy_profile ,
40274142 },
40284143 )
40294144 g .limit_state = build_limit_state (access , "this_month" )
0 commit comments