"""Licensing Advisor Agent — recommends licences, costs, timelines, and sequencing. This agent takes the outputs of the Regulatory Analysis and Token Classification agents and produces actionable licensing recommendations with real-world cost estimates, timelines, prerequisites, and optimal sequencing. """ from src.agents.regulatory import JURISDICTION_FRAMEWORKS # Real licence data — costs, timelines, and prerequisites LICENCE_DATABASE = { "US": { "MSB (FinCEN)": { "regulator": "FinCEN (Financial Crimes Enforcement Network)", "timeline_months": "0.5-1", "estimated_cost_usd": "0 (registration fee)", "total_cost_range": "$5,000 - $25,000 (legal + compliance setup)", "prerequisites": [ "Designate BSA compliance officer", "Develop written AML programme", "Implement CIP/CDD procedures", "Conduct initial BSA/AML risk assessment", ], "notes": "Registration only — does not include state MTL costs. " "Form 107 filing is free but must be renewed every 2 years.", }, "State MTL (per state)": { "regulator": "State banking/financial regulators", "timeline_months": "3-12 (varies by state)", "estimated_cost_usd": "5,000 - 100,000 per state", "total_cost_range": "$50,000 - $1,000,000+ (all 50 states)", "prerequisites": [ "FinCEN MSB registration", "Surety bond (amount varies by state, typically $25K-$500K)", "Minimum net worth requirements (varies by state)", "Background checks for all control persons", "Audited financial statements", "Comprehensive business plan", ], "notes": "NY BitLicense is the most expensive and complex ($5K fee, " "12-18 month timeline, extensive capital requirements). " "Consider NMLS multi-state licensing for efficiency.", }, "SEC Reg D Filing": { "regulator": "SEC", "timeline_months": "0.5 (Form D filing within 15 days of first sale)", "estimated_cost_usd": "0 (SEC filing fee)", "total_cost_range": "$25,000 - $100,000 (legal + offering docs)", "prerequisites": [ "Qualified legal counsel opinion on Reg D applicability", "Offering memorandum / PPM preparation", "Investor accreditation verification procedures (506c)", "Blue sky compliance in each state", ], "notes": "Reg D 506(b) allows up to 35 non-accredited investors. " "Reg D 506(c) permits general solicitation but requires " "verification of accredited investor status.", }, }, "EU": { "CASP (MiCA)": { "regulator": "National Competent Authority (home Member State)", "timeline_months": "3-6", "estimated_cost_usd": "50,000 - 150,000", "total_cost_range": "€50,000 - €150,000 (legal + application + compliance setup)", "prerequisites": [ "Legal entity established in an EU Member State", "Minimum own funds (€50K-€150K depending on services)", "Fit-and-proper assessment of management body", "Written AML/CFT programme compliant with AMLD", "Governance framework with three lines of defence", "ICT security arrangements compliant with DORA", "Client asset segregation procedures", "Complaint handling procedures", "Business continuity plan", "Crypto-asset white paper (if issuing)", ], "notes": "Authorisation in one Member State enables passporting across " "all 27 EU/EEA states. France (AMF) and Ireland (CBI) are " "popular home states. Assessment takes 40 working days for " "complete application. Consider grandfathering provisions " "for firms already operating in EU.", }, }, "UK": { "FCA MLR Registration": { "regulator": "Financial Conduct Authority (FCA)", "timeline_months": "3-6", "estimated_cost_usd": "3,000 - 15,000", "total_cost_range": "£2,000 - £10,000 (FCA fees) + £20,000 - £80,000 (legal/compliance)", "prerequisites": [ "Company registered in the UK or with UK establishment", "Designated MLRO with adequate qualifications", "Written AML/CFT policies and procedures", "Business-wide ML/TF risk assessment", "Fit-and-proper assessment of key individuals", "Adequate financial resources", "Technology and cyber risk assessment", ], "notes": "FCA has an approximately 15-20% approval rate for crypto " "registrations. Common rejection reasons: inadequate AML " "controls, unqualified MLROs, insufficient transaction " "monitoring. Registration ≠ FCA authorisation — separate " "regime under FSMA 2023 coming for full authorisation.", }, }, "SG": { "MAS SPI Licence": { "regulator": "Monetary Authority of Singapore (MAS)", "timeline_months": "6-12", "estimated_cost_usd": "80,000 - 200,000", "total_cost_range": "SGD 100,000 - SGD 300,000 (capital + legal + compliance)", "prerequisites": [ "Company incorporated in Singapore", "Minimum base capital of SGD 100,000", "Application fee of SGD 1,000", "Designated compliance officer", "AML/CFT programme compliant with PSN02", "Technology risk management framework", "Business plan demonstrating viability", "Fit-and-proper assessment of directors and CEO", ], "notes": "SPI licence for DPT services processing dict: """Generate licensing recommendations based on regulatory analysis. Args: regulatory_analysis: Output from RegulatoryAnalysisAgent.analyze(). token_classification: Output from TokenClassificationAgent.classify(). jurisdictions: List of target jurisdiction codes. Returns: Dict containing: - required_licences: list of licence recommendation dicts - exemptions_available: list of potential exemptions - sequencing: list of ordered sequencing recommendations - total_estimated_cost: dict with low/high ranges by jurisdiction """ required_licences = [] exemptions = [] total_cost = {} for jx in jurisdictions: jx_analysis = regulatory_analysis.get(jx, {}) reg_required = jx_analysis.get("registration_required", {}) jx_licences = self._get_required_licences(jx, reg_required, token_classification) required_licences.extend(jx_licences) jx_exemptions = self._check_exemptions(jx, token_classification) exemptions.extend(jx_exemptions) # Calculate costs for this jurisdiction low_cost = 0 high_cost = 0 for lic in jx_licences: low_cost += lic.get("estimated_cost_low_usd", 0) high_cost += lic.get("estimated_cost_high_usd", 0) total_cost[jx] = { "low_usd": low_cost, "high_usd": high_cost, "display": f"${low_cost:,} - ${high_cost:,}", } # Generate sequencing recommendations sequencing = self._recommend_sequencing( jurisdictions, token_classification, required_licences, ) return { "required_licences": required_licences, "exemptions_available": exemptions, "sequencing": sequencing, "total_estimated_cost": total_cost, } def _get_required_licences( self, jx: str, reg_required: dict, token_classification: dict, ) -> list[dict]: """Get the specific licences required in a jurisdiction.""" licences = [] jx_db = LICENCE_DATABASE.get(jx, {}) for licence_type, info in reg_required.items(): if not info.get("required"): continue # Find matching licence in database licence_data = jx_db.get(licence_type, {}) if not licence_data: # Try partial match for db_type, db_data in jx_db.items(): if licence_type.lower() in db_type.lower() or db_type.lower() in licence_type.lower(): licence_data = db_data licence_type = db_type break if licence_data: # Parse cost range cost_range = licence_data.get("total_cost_range", "$0") low, high = self._parse_cost_range(cost_range) licences.append({ "jurisdiction": jx, "jurisdiction_name": JURISDICTION_FRAMEWORKS.get(jx, {}).get("name", jx), "licence_type": licence_type, "regulator": licence_data.get("regulator", ""), "timeline_months": licence_data.get("timeline_months", "Unknown"), "estimated_cost_usd": licence_data.get("estimated_cost_usd", "Unknown"), "estimated_cost_low_usd": low, "estimated_cost_high_usd": high, "total_cost_range": cost_range, "prerequisites": licence_data.get("prerequisites", []), "notes": licence_data.get("notes", ""), "triggers": info.get("triggers", []), }) # Add special cases based on token classification is_security = token_classification.get("is_security", False) if is_security and jx == "US": # Check if SEC registration already in the list has_sec = any(l["licence_type"] == "SEC Reg D Filing" for l in licences) if not has_sec and "SEC Reg D Filing" in jx_db: sec_data = jx_db["SEC Reg D Filing"] low, high = self._parse_cost_range(sec_data.get("total_cost_range", "$0")) licences.append({ "jurisdiction": "US", "jurisdiction_name": "United States", "licence_type": "SEC Reg D Filing", "regulator": sec_data["regulator"], "timeline_months": sec_data["timeline_months"], "estimated_cost_usd": sec_data["estimated_cost_usd"], "estimated_cost_low_usd": low, "estimated_cost_high_usd": high, "total_cost_range": sec_data["total_cost_range"], "prerequisites": sec_data["prerequisites"], "notes": sec_data["notes"], "triggers": ["Token classified as security under Howey Test"], }) return licences def _check_exemptions(self, jx: str, token_classification: dict) -> list[dict]: """Check for available regulatory exemptions.""" exemptions = [] if jx == "US": exemptions.append({ "jurisdiction": "US", "exemption": "Regulation D 506(b) / 506(c)", "description": "Private placement to accredited investors. " "506(b): no general solicitation, up to 35 non-accredited. " "506(c): general solicitation permitted, verified accredited only.", "applicable_if": "Token is classified as a security", }) exemptions.append({ "jurisdiction": "US", "exemption": "Regulation S", "description": "Offshore offering safe harbour — no registration required " "for offers and sales outside the US to non-US persons.", "applicable_if": "Token offering excludes US persons with adequate geo-blocking", }) if jx == "EU": mica_type = token_classification.get("mica_type", "") if mica_type == "utility token": exemptions.append({ "jurisdiction": "EU", "exemption": "MiCA Article 14 — White Paper Exemptions", "description": "White paper not required if: token offered for free, " "mining/staking reward, utility token for existing service, " "offering <€1M over 12 months, or offered only to qualified investors.", "applicable_if": "Token meets specific Article 14 exemption criteria", }) return exemptions def _recommend_sequencing( self, jurisdictions: list[str], token_classification: dict, required_licences: list[dict], ) -> list[str]: """Recommend optimal licensing sequencing.""" sequencing = [] is_security = token_classification.get("is_security", False) # Priority 1: Resolve US classification if security risk if is_security and "US" in jurisdictions: sequencing.append( "1. [IMMEDIATE] Resolve US token classification — obtain formal legal " "opinion and file Reg D/S if token is a security. SEC enforcement has " "global reach and the highest penalties." ) # Priority 2: Quick wins if "UK" in jurisdictions: sequencing.append( f"{'2' if sequencing else '1'}. [MONTH 1-3] Apply for FCA MLR registration " "— relatively quick and affordable, demonstrates regulatory credibility " "to other jurisdictions." ) # Priority 3: EU passport if "EU" in jurisdictions: step_num = len(sequencing) + 1 sequencing.append( f"{step_num}. [MONTH 1-6] Apply for EU CASP authorisation under MiCA — " "single licence provides passporting across all 27 EU/EEA Member States. " "Choose home state strategically (France, Ireland, or Lithuania common)." ) # Priority 4: VARA (can run in parallel) if "AE" in jurisdictions: step_num = len(sequencing) + 1 sequencing.append( f"{step_num}. [MONTH 1-6] Begin VARA licensing process in parallel — " "multi-stage process allows operations under preparatory licence while " "full licence is processed." ) # Priority 5: Singapore (longest, most selective) if "SG" in jurisdictions: step_num = len(sequencing) + 1 sequencing.append( f"{step_num}. [MONTH 1-12] Apply for MAS PSP licence — start early as " "MAS has ~11% approval rate and 6-12 month timeline. Invest heavily in " "AML/compliance infrastructure before applying." ) # Priority 6: US state licences (long tail) if "US" in jurisdictions: step_num = len(sequencing) + 1 sequencing.append( f"{step_num}. [MONTH 3-18] Begin state money transmitter licence applications " "via NMLS. Prioritise high-value states (NY, CA, TX, FL) first. NY BitLicense " "takes 12-18 months — start immediately." ) if not sequencing: sequencing.append( "1. Engage qualified legal counsel in each target jurisdiction " "to confirm licensing requirements." ) return sequencing def _parse_cost_range(self, cost_str: str) -> tuple[int, int]: """Parse a cost range string into (low, high) integers.""" import re numbers = re.findall(r'[\d,]+', cost_str.replace(",", "")) if len(numbers) >= 2: try: return int(numbers[0].replace(",", "")), int(numbers[1].replace(",", "")) except ValueError: pass elif len(numbers) == 1: try: val = int(numbers[0].replace(",", "")) return val, val except ValueError: pass return 0, 0