Spaces:
Sleeping
Sleeping
Upload 72 files
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +1 -0
- README.md +315 -8
- app.py +17 -0
- app/__init__.py +0 -0
- app/__pycache__/__init__.cpython-313.pyc +0 -0
- app/__pycache__/streamlit_app.cpython-313.pyc +0 -0
- app/components/__init__.py +15 -0
- app/components/__pycache__/__init__.cpython-313.pyc +0 -0
- app/components/__pycache__/input_form.cpython-313.pyc +0 -0
- app/components/__pycache__/monitoring_setup.cpython-313.pyc +0 -0
- app/components/__pycache__/results_display.cpython-313.pyc +0 -0
- app/components/input_form.py +223 -0
- app/components/monitoring_setup.py +247 -0
- app/components/results_display.py +374 -0
- app/streamlit_app.py +208 -0
- chroma_db/c30b23e7-6559-433a-b87d-7290f0978053/data_level0.bin +3 -0
- chroma_db/c30b23e7-6559-433a-b87d-7290f0978053/header.bin +3 -0
- chroma_db/c30b23e7-6559-433a-b87d-7290f0978053/length.bin +3 -0
- chroma_db/c30b23e7-6559-433a-b87d-7290f0978053/link_lists.bin +3 -0
- chroma_db/chroma.sqlite3 +3 -0
- config.toml +15 -0
- requirements.txt +32 -0
- src/__init__.py +0 -0
- src/__pycache__/__init__.cpython-313.pyc +0 -0
- src/__pycache__/config.cpython-313.pyc +0 -0
- src/agents/__init__.py +0 -0
- src/agents/__pycache__/__init__.cpython-313.pyc +0 -0
- src/agents/__pycache__/orchestrator.cpython-313.pyc +0 -0
- src/agents/__pycache__/tools.cpython-313.pyc +0 -0
- src/agents/orchestrator.py +621 -0
- src/agents/tools.py +419 -0
- src/analysis/__init__.py +0 -0
- src/analysis/__pycache__/__init__.cpython-313.pyc +0 -0
- src/analysis/__pycache__/compliance_engine.cpython-313.pyc +0 -0
- src/analysis/__pycache__/cost_calculator.cpython-313.pyc +0 -0
- src/analysis/__pycache__/risk_scorer.cpython-313.pyc +0 -0
- src/analysis/__pycache__/token_classifier.cpython-313.pyc +0 -0
- src/analysis/compliance_engine.py +486 -0
- src/analysis/cost_calculator.py +544 -0
- src/analysis/risk_scorer.py +454 -0
- src/analysis/token_classifier.py +487 -0
- src/config.py +70 -0
- src/data/__init__.py +0 -0
- src/data/__pycache__/__init__.cpython-313.pyc +0 -0
- src/data/__pycache__/vectordb.cpython-313.pyc +0 -0
- src/data/regulations/eu/mica-asset-referenced-tokens-2024.json +266 -0
- src/data/regulations/schema.json +178 -0
- src/data/regulations/singapore/mas-cmp-real-estate-tokens-2024.json +275 -0
- src/data/regulations/uae/vara-sto-real-estate-2024.json +229 -0
- src/data/regulations/uk/fca-cis-property-tokens-2024.json +282 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
chroma_db/chroma.sqlite3 filter=lfs diff=lfs merge=lfs -text
|
README.md
CHANGED
|
@@ -1,12 +1,319 @@
|
|
| 1 |
---
|
| 2 |
-
title: Crypto Compliance Agent
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
-
sdk:
|
| 7 |
-
sdk_version:
|
| 8 |
-
app_file: app.py
|
| 9 |
pinned: false
|
|
|
|
| 10 |
---
|
| 11 |
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Crypto Compliance Intelligence Agent
|
| 3 |
+
emoji: 🔒
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: indigo
|
| 6 |
+
sdk: streamlit
|
| 7 |
+
sdk_version: 1.29.0
|
| 8 |
+
app_file: app/streamlit_app.py
|
| 9 |
pinned: false
|
| 10 |
+
license: mit
|
| 11 |
---
|
| 12 |
|
| 13 |
+
# Crypto Compliance Intelligence Agent
|
| 14 |
+
|
| 15 |
+
> Multi-jurisdiction crypto regulatory compliance analysis powered by AI agents and RAG
|
| 16 |
+
|
| 17 |
+
---
|
| 18 |
+
|
| 19 |
+
## Overview
|
| 20 |
+
|
| 21 |
+
An AI-powered system that helps crypto businesses navigate complex regulatory requirements across multiple jurisdictions. Get instant compliance analysis, risk scoring, and cost estimates in under 60 seconds.
|
| 22 |
+
|
| 23 |
+
**Supported Jurisdictions**: US (SEC + State MTLs) | EU (MiCA) | Singapore (MAS) | UK (FCA) | UAE (VARA)
|
| 24 |
+
|
| 25 |
+
---
|
| 26 |
+
|
| 27 |
+
## Features
|
| 28 |
+
|
| 29 |
+
- **Multi-Jurisdiction Analysis**: Comprehensive compliance mapping across US, EU, Singapore, and UK
|
| 30 |
+
- **AI Agent Architecture**: LangGraph-powered agent with tool use and reasoning
|
| 31 |
+
- **RAG-Based Retrieval**: Semantic search over regulatory documents using ChromaDB
|
| 32 |
+
- **Token Classification**: Automated Howey Test analysis for security determination
|
| 33 |
+
- **Risk Scoring**: 0-100 weighted risk assessment based on gaps and severity
|
| 34 |
+
- **Cost Estimation**: First-year and ongoing compliance cost breakdowns
|
| 35 |
+
- **Regulatory Monitoring**: Automated scraping of SEC, MiCA, MAS, and FCA sources
|
| 36 |
+
- **Explainable AI**: Full agent reasoning chain visible in results
|
| 37 |
+
|
| 38 |
+
---
|
| 39 |
+
|
| 40 |
+
## Tech Stack
|
| 41 |
+
|
| 42 |
+
| Component | Technology |
|
| 43 |
+
|-----------|------------|
|
| 44 |
+
| **LLM** | Google Gemini Flash 2.5 (2M context window) |
|
| 45 |
+
| **Agent Framework** | LangGraph (state machines + tool use) |
|
| 46 |
+
| **Vector Database** | ChromaDB (local persistence) |
|
| 47 |
+
| **Embeddings** | sentence-transformers/all-MiniLM-L6-v2 |
|
| 48 |
+
| **NLP Models** | FinBERT, Legal-BERT |
|
| 49 |
+
| **Web Framework** | Streamlit |
|
| 50 |
+
| **Data Processing** | pandas, pdfplumber, BeautifulSoup4 |
|
| 51 |
+
| **Deployment** | HuggingFace Spaces |
|
| 52 |
+
|
| 53 |
+
---
|
| 54 |
+
|
| 55 |
+
## Quick Start
|
| 56 |
+
|
| 57 |
+
### Prerequisites
|
| 58 |
+
|
| 59 |
+
- Python 3.11+ (tested on 3.13.5)
|
| 60 |
+
- Google Gemini API key ([Get it here](https://makersuite.google.com/app/apikey))
|
| 61 |
+
- HuggingFace token (optional, for model downloads)
|
| 62 |
+
|
| 63 |
+
### Installation
|
| 64 |
+
|
| 65 |
+
```bash
|
| 66 |
+
# Clone repository
|
| 67 |
+
git clone https://github.com/yourusername/crypto-compliance-agent.git
|
| 68 |
+
cd crypto-compliance-agent
|
| 69 |
+
|
| 70 |
+
# Create virtual environment
|
| 71 |
+
python -m venv venv
|
| 72 |
+
source venv/bin/activate # Windows: venv\Scripts\activate
|
| 73 |
+
|
| 74 |
+
# Install dependencies
|
| 75 |
+
pip install -r requirements.txt
|
| 76 |
+
|
| 77 |
+
# Configure environment
|
| 78 |
+
cp .env.example .env
|
| 79 |
+
# Edit .env and add your GEMINI_API_KEY
|
| 80 |
+
```
|
| 81 |
+
|
| 82 |
+
### Initialize Database
|
| 83 |
+
|
| 84 |
+
```bash
|
| 85 |
+
# Populate ChromaDB with regulatory data
|
| 86 |
+
python scripts/setup_vectordb.py
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
### Run Locally
|
| 90 |
+
|
| 91 |
+
```bash
|
| 92 |
+
streamlit run app/streamlit_app.py
|
| 93 |
+
```
|
| 94 |
+
|
| 95 |
+
Navigate to `http://localhost:8501`
|
| 96 |
+
|
| 97 |
+
---
|
| 98 |
+
|
| 99 |
+
## Project Structure
|
| 100 |
+
|
| 101 |
+
```
|
| 102 |
+
crypto-compliance-agent/
|
| 103 |
+
├── src/
|
| 104 |
+
│ ├── config.py # Configuration management
|
| 105 |
+
│ ├── models/ # LLM and embeddings
|
| 106 |
+
│ │ ├── llm.py # Gemini integration
|
| 107 |
+
│ │ └── embeddings.py # Sentence transformers
|
| 108 |
+
│ ├── agents/ # LangGraph agents
|
| 109 |
+
│ │ ├── orchestrator.py # Main agent coordinator
|
| 110 |
+
│ │ ├── tools.py # Agent tools (search, calculate, etc.)
|
| 111 |
+
│ │ ├── classifier.py # Activity/token classification
|
| 112 |
+
│ │ └── analyzer.py # Compliance analysis
|
| 113 |
+
│ ├── data/ # Data layer
|
| 114 |
+
│ │ ├── vectordb.py # ChromaDB interface
|
| 115 |
+
│ │ ├── scrapers/ # Regulatory source scrapers
|
| 116 |
+
│ │ │ ├── sec.py # US SEC
|
| 117 |
+
│ │ │ ├── mica.py # EU MiCA
|
| 118 |
+
│ │ │ ├── mas.py # Singapore MAS
|
| 119 |
+
│ │ │ └── fca.py # UK FCA
|
| 120 |
+
│ │ └── regulations/ # Regulatory knowledge base
|
| 121 |
+
│ │ ├── us/
|
| 122 |
+
│ │ ├── eu/
|
| 123 |
+
│ │ ├── singapore/
|
| 124 |
+
│ │ └── uk/
|
| 125 |
+
│ ├── processors/ # Document processing
|
| 126 |
+
│ │ ├── document_parser.py # PDF/text extraction
|
| 127 |
+
│ │ └── entity_extraction.py # FinBERT NER
|
| 128 |
+
│ ├── analysis/ # Analysis engines
|
| 129 |
+
│ │ ├── compliance_engine.py # Rule matching
|
| 130 |
+
│ │ ├── risk_scorer.py # Risk calculation
|
| 131 |
+
│ │ ├── token_classifier.py # Howey Test
|
| 132 |
+
│ │ └── cost_calculator.py # Cost estimation
|
| 133 |
+
│ └── utils/ # Utilities
|
| 134 |
+
├── app/
|
| 135 |
+
│ ├── streamlit_app.py # Main UI entry point
|
| 136 |
+
│ └── components/ # UI components
|
| 137 |
+
│ ├── input_form.py
|
| 138 |
+
│ ├── results_display.py
|
| 139 |
+
│ └── monitoring_setup.py
|
| 140 |
+
├── docs/
|
| 141 |
+
│ ├── system_index.md # Technical documentation
|
| 142 |
+
│ ├── navigation_guide.md # Non-technical guide
|
| 143 |
+
│ └── project_explanation.md # Detailed explanation
|
| 144 |
+
├── context/
|
| 145 |
+
│ ├── development_log.md # Project history
|
| 146 |
+
│ ├── resume_context.md # Session recovery (not committed)
|
| 147 |
+
│ └── portfolio_doc.md # Career documentation (not committed)
|
| 148 |
+
├── tests/ # Unit tests
|
| 149 |
+
├── scripts/ # Utility scripts
|
| 150 |
+
│ ├── setup_vectordb.py
|
| 151 |
+
│ ├── update_regulations.py
|
| 152 |
+
│ └── test_agent.py
|
| 153 |
+
└── requirements.txt
|
| 154 |
+
```
|
| 155 |
+
|
| 156 |
+
---
|
| 157 |
+
|
| 158 |
+
## Usage Example
|
| 159 |
+
|
| 160 |
+
### Input
|
| 161 |
+
|
| 162 |
+
```python
|
| 163 |
+
{
|
| 164 |
+
"jurisdictions": ["United States", "European Union"],
|
| 165 |
+
"activities": ["NFT marketplace", "Crypto payment processor"],
|
| 166 |
+
"description": "Users buy and sell NFTs using credit cards and crypto. We take a 2% fee.",
|
| 167 |
+
"token_info": None
|
| 168 |
+
}
|
| 169 |
+
```
|
| 170 |
+
|
| 171 |
+
### Output
|
| 172 |
+
|
| 173 |
+
```
|
| 174 |
+
Risk Score: 65/100 (Medium-High)
|
| 175 |
+
Compliance Gaps: 4
|
| 176 |
+
Estimated First-Year Cost: $250,000 - $550,000
|
| 177 |
+
|
| 178 |
+
US Compliance:
|
| 179 |
+
❌ FinCEN MSB Registration Required ($5k-$10k)
|
| 180 |
+
❌ State MTL Licenses (15 states targeted: $750k-$2.25M)
|
| 181 |
+
✅ No SEC registration (NFTs not securities)
|
| 182 |
+
|
| 183 |
+
EU Compliance:
|
| 184 |
+
❌ MiCA CASP Authorization (€100k-€300k)
|
| 185 |
+
❌ AML/KYC Compliance (€50k/year ongoing)
|
| 186 |
+
|
| 187 |
+
Recommendations:
|
| 188 |
+
1. File FinCEN MSB within 180 days
|
| 189 |
+
2. Prioritize MTL applications for high-volume states (CA, NY, TX)
|
| 190 |
+
3. Engage MiCA legal counsel in EU member state of choice
|
| 191 |
+
4. Implement KYC/AML procedures before EU launch
|
| 192 |
+
```
|
| 193 |
+
|
| 194 |
+
---
|
| 195 |
+
|
| 196 |
+
## Documentation
|
| 197 |
+
|
| 198 |
+
- **[System Index](docs/system_index.md)**: Technical architecture and API reference
|
| 199 |
+
- **[Navigation Guide](docs/navigation_guide.md)**: Non-technical codebase guide
|
| 200 |
+
- **[Project Explanation](docs/project_explanation.md)**: Detailed project overview
|
| 201 |
+
|
| 202 |
+
---
|
| 203 |
+
|
| 204 |
+
## Development
|
| 205 |
+
|
| 206 |
+
### Running Tests
|
| 207 |
+
|
| 208 |
+
```bash
|
| 209 |
+
pytest tests/
|
| 210 |
+
```
|
| 211 |
+
|
| 212 |
+
### Code Quality
|
| 213 |
+
|
| 214 |
+
```bash
|
| 215 |
+
# Format code
|
| 216 |
+
black src/ app/ tests/
|
| 217 |
+
|
| 218 |
+
# Lint
|
| 219 |
+
flake8 src/ app/ tests/
|
| 220 |
+
```
|
| 221 |
+
|
| 222 |
+
### Update Regulations
|
| 223 |
+
|
| 224 |
+
```bash
|
| 225 |
+
# Run weekly to fetch latest regulations
|
| 226 |
+
python scripts/update_regulations.py
|
| 227 |
+
```
|
| 228 |
+
|
| 229 |
+
---
|
| 230 |
+
|
| 231 |
+
## Deployment
|
| 232 |
+
|
| 233 |
+
### HuggingFace Spaces
|
| 234 |
+
|
| 235 |
+
1. Create new Space: [https://huggingface.co/new-space](https://huggingface.co/new-space)
|
| 236 |
+
2. Select **Streamlit** SDK
|
| 237 |
+
3. Link GitHub repository
|
| 238 |
+
4. Add secrets in Space settings:
|
| 239 |
+
- `GEMINI_API_KEY`
|
| 240 |
+
- `HF_TOKEN` (optional)
|
| 241 |
+
5. Deploy
|
| 242 |
+
|
| 243 |
+
### Environment Variables
|
| 244 |
+
|
| 245 |
+
Required:
|
| 246 |
+
- `GEMINI_API_KEY`: Google Gemini API key
|
| 247 |
+
|
| 248 |
+
Optional:
|
| 249 |
+
- `HF_TOKEN`: HuggingFace token
|
| 250 |
+
- `GITHUB_TOKEN`: GitHub access (for scrapers)
|
| 251 |
+
- `LOG_LEVEL`: Logging level (default: INFO)
|
| 252 |
+
|
| 253 |
+
---
|
| 254 |
+
|
| 255 |
+
## Important Disclaimers
|
| 256 |
+
|
| 257 |
+
### ⚠️ NOT LEGAL ADVICE
|
| 258 |
+
|
| 259 |
+
This tool provides **general information only** and does NOT constitute legal, financial, or regulatory advice.
|
| 260 |
+
|
| 261 |
+
- **No Warranties**: Accuracy not guaranteed. Regulations change frequently.
|
| 262 |
+
- **No Liability**: Users assume all risk. Creators not liable for compliance failures, fines, or legal issues.
|
| 263 |
+
- **Consult Lawyers**: Always consult qualified legal counsel before making compliance decisions.
|
| 264 |
+
|
| 265 |
+
Use at your own risk.
|
| 266 |
+
|
| 267 |
+
---
|
| 268 |
+
|
| 269 |
+
## Roadmap
|
| 270 |
+
|
| 271 |
+
- [x] Phase 1: Foundation setup
|
| 272 |
+
- [x] Phase 2: Regulatory data layer (5 jurisdictions, ChromaDB)
|
| 273 |
+
- [x] Phase 3: NLP & entity extraction (Howey Test, entity recognition)
|
| 274 |
+
- [x] Phase 4: Agent architecture (LangGraph, 6 nodes, 6 tools)
|
| 275 |
+
- [x] Phase 5: Analysis & scoring engine (ComplianceEngine, RiskScorer, CostCalculator)
|
| 276 |
+
- [x] Phase 6: Streamlit UI (3-tab interface, Plotly visualizations)
|
| 277 |
+
- [x] Phase 7: Testing & optimization (Integration tests, performance validation)
|
| 278 |
+
- [ ] Phase 8: Deployment to HuggingFace Spaces (IN PROGRESS)
|
| 279 |
+
|
| 280 |
+
See `context/development_log.md` for detailed progress.
|
| 281 |
+
|
| 282 |
+
---
|
| 283 |
+
|
| 284 |
+
## Contributing
|
| 285 |
+
|
| 286 |
+
Contributions welcome! Please:
|
| 287 |
+
|
| 288 |
+
1. Fork the repository
|
| 289 |
+
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
| 290 |
+
3. Commit changes (`git commit -m 'Add amazing feature'`)
|
| 291 |
+
4. Push to branch (`git push origin feature/amazing-feature`)
|
| 292 |
+
5. Open a Pull Request
|
| 293 |
+
|
| 294 |
+
---
|
| 295 |
+
|
| 296 |
+
## License
|
| 297 |
+
|
| 298 |
+
[To be determined]
|
| 299 |
+
|
| 300 |
+
---
|
| 301 |
+
|
| 302 |
+
## Contact
|
| 303 |
+
|
| 304 |
+
- **Issues**: [GitHub Issues](https://github.com/yourusername/crypto-compliance-agent/issues)
|
| 305 |
+
- **Documentation**: `docs/` folder
|
| 306 |
+
|
| 307 |
+
---
|
| 308 |
+
|
| 309 |
+
## Acknowledgments
|
| 310 |
+
|
| 311 |
+
- **Google Gemini**: LLM provider
|
| 312 |
+
- **LangChain/LangGraph**: Agent framework
|
| 313 |
+
- **ChromaDB**: Vector database
|
| 314 |
+
- **HuggingFace**: Model hosting and deployment
|
| 315 |
+
- **Regulatory Sources**: SEC, EU Official Journal, MAS, FCA
|
| 316 |
+
|
| 317 |
+
---
|
| 318 |
+
|
| 319 |
+
**Built with AI assistance** (Claude Code) | **Status**: Phases 1-7 Complete - Ready for Deployment | **Last Updated**: 2025-10-22
|
app.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Entry point for HuggingFace Spaces deployment.
|
| 3 |
+
Redirects to the main Streamlit app.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import subprocess
|
| 7 |
+
import sys
|
| 8 |
+
|
| 9 |
+
if __name__ == "__main__":
|
| 10 |
+
# Run the Streamlit app
|
| 11 |
+
subprocess.run([
|
| 12 |
+
sys.executable, "-m", "streamlit", "run",
|
| 13 |
+
"app/streamlit_app.py",
|
| 14 |
+
"--server.headless=true",
|
| 15 |
+
"--server.port=7860",
|
| 16 |
+
"--server.enableCORS=false"
|
| 17 |
+
])
|
app/__init__.py
ADDED
|
File without changes
|
app/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (191 Bytes). View file
|
|
|
app/__pycache__/streamlit_app.cpython-313.pyc
ADDED
|
Binary file (8.81 kB). View file
|
|
|
app/components/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
UI Components for Streamlit app
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from app.components.input_form import render_input_form, get_jurisdiction_display_name, get_activity_display_name
|
| 6 |
+
from app.components.results_display import render_results
|
| 7 |
+
from app.components.monitoring_setup import render_monitoring_setup
|
| 8 |
+
|
| 9 |
+
__all__ = [
|
| 10 |
+
'render_input_form',
|
| 11 |
+
'render_results',
|
| 12 |
+
'render_monitoring_setup',
|
| 13 |
+
'get_jurisdiction_display_name',
|
| 14 |
+
'get_activity_display_name'
|
| 15 |
+
]
|
app/components/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (608 Bytes). View file
|
|
|
app/components/__pycache__/input_form.cpython-313.pyc
ADDED
|
Binary file (9.75 kB). View file
|
|
|
app/components/__pycache__/monitoring_setup.cpython-313.pyc
ADDED
|
Binary file (10.4 kB). View file
|
|
|
app/components/__pycache__/results_display.cpython-313.pyc
ADDED
|
Binary file (17.3 kB). View file
|
|
|
app/components/input_form.py
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Input Form Component
|
| 3 |
+
|
| 4 |
+
Renders the user input form for compliance analysis.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import streamlit as st
|
| 8 |
+
from typing import Dict, Optional
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def render_input_form() -> Optional[Dict]:
|
| 12 |
+
"""
|
| 13 |
+
Render input form for compliance analysis.
|
| 14 |
+
|
| 15 |
+
Returns:
|
| 16 |
+
Dictionary with form data if submitted, None otherwise
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
with st.form("compliance_form"):
|
| 20 |
+
st.markdown("### Business Information")
|
| 21 |
+
|
| 22 |
+
# Jurisdiction selection
|
| 23 |
+
st.markdown("#### 1. Select Target Jurisdictions")
|
| 24 |
+
st.caption("Choose all jurisdictions where you plan to operate")
|
| 25 |
+
|
| 26 |
+
col1, col2 = st.columns(2)
|
| 27 |
+
|
| 28 |
+
with col1:
|
| 29 |
+
us_selected = st.checkbox("🇺🇸 United States (SEC)", value=False)
|
| 30 |
+
eu_selected = st.checkbox("🇪🇺 European Union (MiCA)", value=False)
|
| 31 |
+
singapore_selected = st.checkbox("🇸🇬 Singapore (MAS)", value=False)
|
| 32 |
+
|
| 33 |
+
with col2:
|
| 34 |
+
uk_selected = st.checkbox("🇬🇧 United Kingdom (FCA)", value=False)
|
| 35 |
+
uae_selected = st.checkbox("🇦🇪 UAE (VARA)", value=False)
|
| 36 |
+
|
| 37 |
+
# Build jurisdictions list
|
| 38 |
+
jurisdictions = []
|
| 39 |
+
if us_selected:
|
| 40 |
+
jurisdictions.append('us')
|
| 41 |
+
if eu_selected:
|
| 42 |
+
jurisdictions.append('eu')
|
| 43 |
+
if singapore_selected:
|
| 44 |
+
jurisdictions.append('singapore')
|
| 45 |
+
if uk_selected:
|
| 46 |
+
jurisdictions.append('uk')
|
| 47 |
+
if uae_selected:
|
| 48 |
+
jurisdictions.append('uae')
|
| 49 |
+
|
| 50 |
+
st.markdown("---")
|
| 51 |
+
|
| 52 |
+
# Activity selection
|
| 53 |
+
st.markdown("#### 2. Select Crypto Activities")
|
| 54 |
+
st.caption("Check all activities your business will perform")
|
| 55 |
+
|
| 56 |
+
col1, col2, col3 = st.columns(3)
|
| 57 |
+
|
| 58 |
+
with col1:
|
| 59 |
+
exchange = st.checkbox("💱 Exchange/Trading Platform", value=False)
|
| 60 |
+
custody = st.checkbox("🔐 Custody Services", value=False)
|
| 61 |
+
staking = st.checkbox("📊 Staking Services", value=False)
|
| 62 |
+
lending = st.checkbox("💰 Lending", value=False)
|
| 63 |
+
borrowing = st.checkbox("📈 Borrowing", value=False)
|
| 64 |
+
|
| 65 |
+
with col2:
|
| 66 |
+
payment = st.checkbox("💳 Payment Processing", value=False)
|
| 67 |
+
token_issuance = st.checkbox("🪙 Token Issuance", value=False)
|
| 68 |
+
mining = st.checkbox("⛏️ Mining", value=False)
|
| 69 |
+
nft = st.checkbox("🖼️ NFT Marketplace", value=False)
|
| 70 |
+
|
| 71 |
+
with col3:
|
| 72 |
+
defi = st.checkbox("🔄 DeFi Protocol", value=False)
|
| 73 |
+
derivatives = st.checkbox("📉 Derivatives", value=False)
|
| 74 |
+
otc = st.checkbox("🤝 OTC Trading", value=False)
|
| 75 |
+
wallet = st.checkbox("👛 Wallet Services", value=False)
|
| 76 |
+
|
| 77 |
+
# Build activities list
|
| 78 |
+
activities = []
|
| 79 |
+
if exchange:
|
| 80 |
+
activities.append('exchange')
|
| 81 |
+
if custody:
|
| 82 |
+
activities.append('custody')
|
| 83 |
+
if staking:
|
| 84 |
+
activities.append('staking')
|
| 85 |
+
if lending:
|
| 86 |
+
activities.append('lending')
|
| 87 |
+
if borrowing:
|
| 88 |
+
activities.append('borrowing')
|
| 89 |
+
if payment:
|
| 90 |
+
activities.append('payment_processing')
|
| 91 |
+
if token_issuance:
|
| 92 |
+
activities.append('token_issuance')
|
| 93 |
+
if mining:
|
| 94 |
+
activities.append('mining')
|
| 95 |
+
if nft:
|
| 96 |
+
activities.append('nft_marketplace')
|
| 97 |
+
if defi:
|
| 98 |
+
activities.append('defi_protocol')
|
| 99 |
+
if derivatives:
|
| 100 |
+
activities.append('derivatives')
|
| 101 |
+
if otc:
|
| 102 |
+
activities.append('otc_trading')
|
| 103 |
+
if wallet:
|
| 104 |
+
activities.append('wallet_services')
|
| 105 |
+
|
| 106 |
+
st.markdown("---")
|
| 107 |
+
|
| 108 |
+
# Business description
|
| 109 |
+
st.markdown("#### 3. Describe Your Business")
|
| 110 |
+
st.caption("Provide a detailed description of your business model, target users, and how your platform works")
|
| 111 |
+
|
| 112 |
+
business_description = st.text_area(
|
| 113 |
+
"Business Description",
|
| 114 |
+
height=150,
|
| 115 |
+
placeholder="Example: We're building a DeFi lending protocol where users can deposit USDC and earn 6% APY. "
|
| 116 |
+
"Borrowers can take loans at 8% interest rate. We'll issue a governance token to early users.",
|
| 117 |
+
label_visibility="collapsed"
|
| 118 |
+
)
|
| 119 |
+
|
| 120 |
+
st.markdown("---")
|
| 121 |
+
|
| 122 |
+
# Token information (optional)
|
| 123 |
+
st.markdown("#### 4. Token Information (Optional)")
|
| 124 |
+
st.caption("If you're issuing a token, provide details for classification analysis")
|
| 125 |
+
|
| 126 |
+
issue_token = st.checkbox("I am issuing a token", value=False)
|
| 127 |
+
|
| 128 |
+
token_description = None
|
| 129 |
+
if issue_token:
|
| 130 |
+
col1, col2 = st.columns(2)
|
| 131 |
+
|
| 132 |
+
with col1:
|
| 133 |
+
token_name = st.text_input("Token Name", placeholder="e.g., MyToken")
|
| 134 |
+
token_symbol = st.text_input("Token Symbol", placeholder="e.g., MTK")
|
| 135 |
+
|
| 136 |
+
with col2:
|
| 137 |
+
token_use_case = st.selectbox(
|
| 138 |
+
"Primary Use Case",
|
| 139 |
+
["Utility", "Governance", "Payment", "Security/Investment", "Hybrid"]
|
| 140 |
+
)
|
| 141 |
+
|
| 142 |
+
token_details = st.text_area(
|
| 143 |
+
"Token Details",
|
| 144 |
+
height=100,
|
| 145 |
+
placeholder="Describe token distribution, vesting, utility, and how holders benefit",
|
| 146 |
+
help="Include: Distribution model, vesting schedules, utility within platform, rights/benefits for holders"
|
| 147 |
+
)
|
| 148 |
+
|
| 149 |
+
# Build token description
|
| 150 |
+
if token_name or token_details:
|
| 151 |
+
token_description = f"Token Name: {token_name or 'N/A'}\n"
|
| 152 |
+
token_description += f"Token Symbol: {token_symbol or 'N/A'}\n"
|
| 153 |
+
token_description += f"Primary Use Case: {token_use_case}\n"
|
| 154 |
+
token_description += f"Details: {token_details or 'N/A'}"
|
| 155 |
+
|
| 156 |
+
st.markdown("---")
|
| 157 |
+
|
| 158 |
+
# Submit button
|
| 159 |
+
submitted = st.form_submit_button("🚀 Analyze Compliance", use_container_width=True)
|
| 160 |
+
|
| 161 |
+
if submitted:
|
| 162 |
+
# Validation
|
| 163 |
+
errors = []
|
| 164 |
+
|
| 165 |
+
if not jurisdictions:
|
| 166 |
+
errors.append("⚠️ Please select at least one jurisdiction")
|
| 167 |
+
|
| 168 |
+
if not activities:
|
| 169 |
+
errors.append("⚠️ Please select at least one crypto activity")
|
| 170 |
+
|
| 171 |
+
if not business_description or len(business_description) < 50:
|
| 172 |
+
errors.append("⚠️ Please provide a detailed business description (at least 50 characters)")
|
| 173 |
+
|
| 174 |
+
if issue_token and not token_details:
|
| 175 |
+
errors.append("⚠️ Please provide token details if you're issuing a token")
|
| 176 |
+
|
| 177 |
+
if errors:
|
| 178 |
+
for error in errors:
|
| 179 |
+
st.error(error)
|
| 180 |
+
return None
|
| 181 |
+
|
| 182 |
+
# Return form data
|
| 183 |
+
return {
|
| 184 |
+
'jurisdictions': jurisdictions,
|
| 185 |
+
'activities': activities,
|
| 186 |
+
'business_description': business_description,
|
| 187 |
+
'token_description': token_description,
|
| 188 |
+
'issue_token': issue_token
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
return None
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
def get_jurisdiction_display_name(jurisdiction: str) -> str:
|
| 195 |
+
"""Get display name for jurisdiction."""
|
| 196 |
+
mapping = {
|
| 197 |
+
'us': '🇺🇸 United States',
|
| 198 |
+
'eu': '🇪🇺 European Union',
|
| 199 |
+
'singapore': '🇸🇬 Singapore',
|
| 200 |
+
'uk': '🇬🇧 United Kingdom',
|
| 201 |
+
'uae': '🇦🇪 UAE'
|
| 202 |
+
}
|
| 203 |
+
return mapping.get(jurisdiction, jurisdiction.upper())
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
def get_activity_display_name(activity: str) -> str:
|
| 207 |
+
"""Get display name for activity."""
|
| 208 |
+
mapping = {
|
| 209 |
+
'exchange': '💱 Exchange/Trading',
|
| 210 |
+
'custody': '🔐 Custody',
|
| 211 |
+
'staking': '📊 Staking',
|
| 212 |
+
'lending': '💰 Lending',
|
| 213 |
+
'borrowing': '📈 Borrowing',
|
| 214 |
+
'payment_processing': '💳 Payment Processing',
|
| 215 |
+
'token_issuance': '🪙 Token Issuance',
|
| 216 |
+
'mining': '⛏️ Mining',
|
| 217 |
+
'nft_marketplace': '🖼️ NFT Marketplace',
|
| 218 |
+
'defi_protocol': '🔄 DeFi Protocol',
|
| 219 |
+
'derivatives': '📉 Derivatives',
|
| 220 |
+
'otc_trading': '🤝 OTC Trading',
|
| 221 |
+
'wallet_services': '👛 Wallet Services'
|
| 222 |
+
}
|
| 223 |
+
return mapping.get(activity, activity.replace('_', ' ').title())
|
app/components/monitoring_setup.py
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Monitoring Setup Component
|
| 3 |
+
|
| 4 |
+
Allows users to set up alerts for regulatory changes.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import streamlit as st
|
| 8 |
+
from datetime import datetime
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def render_monitoring_setup():
|
| 12 |
+
"""
|
| 13 |
+
Render monitoring and alert configuration interface.
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
st.markdown("""
|
| 17 |
+
Stay informed about regulatory changes that may affect your business.
|
| 18 |
+
Configure alerts to receive notifications about new regulations, deadlines, and compliance updates.
|
| 19 |
+
""")
|
| 20 |
+
|
| 21 |
+
st.markdown("---")
|
| 22 |
+
|
| 23 |
+
# Email alerts section
|
| 24 |
+
st.markdown("### 📧 Email Alerts")
|
| 25 |
+
|
| 26 |
+
with st.form("email_alerts_form"):
|
| 27 |
+
st.markdown("Receive email notifications for regulatory updates")
|
| 28 |
+
|
| 29 |
+
email = st.text_input(
|
| 30 |
+
"Email Address",
|
| 31 |
+
placeholder="your.email@company.com"
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
st.markdown("**Alert Frequency:**")
|
| 35 |
+
frequency = st.radio(
|
| 36 |
+
"How often would you like to receive alerts?",
|
| 37 |
+
["Immediate (as updates occur)", "Daily digest", "Weekly summary"],
|
| 38 |
+
label_visibility="collapsed"
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
st.markdown("**Jurisdictions to Monitor:**")
|
| 42 |
+
col1, col2 = st.columns(2)
|
| 43 |
+
|
| 44 |
+
with col1:
|
| 45 |
+
monitor_us = st.checkbox("🇺🇸 United States", value=False)
|
| 46 |
+
monitor_eu = st.checkbox("🇪🇺 European Union", value=False)
|
| 47 |
+
monitor_singapore = st.checkbox("🇸🇬 Singapore", value=False)
|
| 48 |
+
|
| 49 |
+
with col2:
|
| 50 |
+
monitor_uk = st.checkbox("🇬🇧 United Kingdom", value=False)
|
| 51 |
+
monitor_uae = st.checkbox("🇦🇪 UAE", value=False)
|
| 52 |
+
|
| 53 |
+
st.markdown("**Activity Types:**")
|
| 54 |
+
activities = st.multiselect(
|
| 55 |
+
"Select activities to monitor",
|
| 56 |
+
[
|
| 57 |
+
"Exchange/Trading",
|
| 58 |
+
"Custody",
|
| 59 |
+
"Staking",
|
| 60 |
+
"Lending/Borrowing",
|
| 61 |
+
"Token Issuance",
|
| 62 |
+
"DeFi",
|
| 63 |
+
"NFTs",
|
| 64 |
+
"All Activities"
|
| 65 |
+
],
|
| 66 |
+
label_visibility="collapsed"
|
| 67 |
+
)
|
| 68 |
+
|
| 69 |
+
submit_email = st.form_submit_button("🔔 Subscribe to Alerts", use_container_width=True)
|
| 70 |
+
|
| 71 |
+
if submit_email:
|
| 72 |
+
if email and '@' in email:
|
| 73 |
+
st.success(f"✅ Successfully subscribed {email} to regulatory alerts!")
|
| 74 |
+
st.info("📬 You'll receive a confirmation email shortly.")
|
| 75 |
+
else:
|
| 76 |
+
st.error("⚠️ Please enter a valid email address")
|
| 77 |
+
|
| 78 |
+
st.markdown("---")
|
| 79 |
+
|
| 80 |
+
# RSS Feed section
|
| 81 |
+
st.markdown("### 📰 RSS Feed")
|
| 82 |
+
|
| 83 |
+
st.markdown("""
|
| 84 |
+
Subscribe to our RSS feed to stay updated using your preferred feed reader.
|
| 85 |
+
The feed includes:
|
| 86 |
+
- New regulations and proposed rules
|
| 87 |
+
- Effective date changes
|
| 88 |
+
- Enforcement actions
|
| 89 |
+
- Compliance deadlines
|
| 90 |
+
""")
|
| 91 |
+
|
| 92 |
+
# Generate RSS feed URL (placeholder)
|
| 93 |
+
rss_url = "https://compliance-agent.example.com/feed/regulations.xml"
|
| 94 |
+
|
| 95 |
+
col1, col2 = st.columns([3, 1])
|
| 96 |
+
|
| 97 |
+
with col1:
|
| 98 |
+
st.code(rss_url, language=None)
|
| 99 |
+
|
| 100 |
+
with col2:
|
| 101 |
+
st.button("📋 Copy URL", use_container_width=True)
|
| 102 |
+
|
| 103 |
+
st.caption("Popular RSS readers: Feedly, Inoreader, NewsBlur, RSS Reader")
|
| 104 |
+
|
| 105 |
+
st.markdown("---")
|
| 106 |
+
|
| 107 |
+
# Webhook section
|
| 108 |
+
st.markdown("### 🔗 Webhook Integration")
|
| 109 |
+
|
| 110 |
+
st.markdown("""
|
| 111 |
+
Set up webhooks to receive real-time regulatory updates in your own systems.
|
| 112 |
+
Ideal for enterprise integrations and custom alerting workflows.
|
| 113 |
+
""")
|
| 114 |
+
|
| 115 |
+
with st.form("webhook_form"):
|
| 116 |
+
webhook_url = st.text_input(
|
| 117 |
+
"Webhook URL",
|
| 118 |
+
placeholder="https://your-domain.com/api/webhooks/compliance"
|
| 119 |
+
)
|
| 120 |
+
|
| 121 |
+
webhook_secret = st.text_input(
|
| 122 |
+
"Secret Key (optional)",
|
| 123 |
+
type="password",
|
| 124 |
+
placeholder="Used to verify webhook authenticity"
|
| 125 |
+
)
|
| 126 |
+
|
| 127 |
+
st.markdown("**Trigger Events:**")
|
| 128 |
+
col1, col2 = st.columns(2)
|
| 129 |
+
|
| 130 |
+
with col1:
|
| 131 |
+
trigger_new = st.checkbox("New regulations published", value=True)
|
| 132 |
+
trigger_updated = st.checkbox("Regulations updated", value=True)
|
| 133 |
+
|
| 134 |
+
with col2:
|
| 135 |
+
trigger_deadline = st.checkbox("Upcoming deadlines (7 days)", value=True)
|
| 136 |
+
trigger_enforcement = st.checkbox("Enforcement actions", value=False)
|
| 137 |
+
|
| 138 |
+
submit_webhook = st.form_submit_button("🔌 Configure Webhook", use_container_width=True)
|
| 139 |
+
|
| 140 |
+
if submit_webhook:
|
| 141 |
+
if webhook_url and webhook_url.startswith('http'):
|
| 142 |
+
st.success("✅ Webhook configured successfully!")
|
| 143 |
+
st.json({
|
| 144 |
+
"url": webhook_url,
|
| 145 |
+
"events": [
|
| 146 |
+
e for e, enabled in [
|
| 147 |
+
("regulation.new", trigger_new),
|
| 148 |
+
("regulation.updated", trigger_updated),
|
| 149 |
+
("deadline.upcoming", trigger_deadline),
|
| 150 |
+
("enforcement.action", trigger_enforcement)
|
| 151 |
+
] if enabled
|
| 152 |
+
],
|
| 153 |
+
"created_at": datetime.now().isoformat()
|
| 154 |
+
})
|
| 155 |
+
else:
|
| 156 |
+
st.error("⚠️ Please enter a valid HTTPS webhook URL")
|
| 157 |
+
|
| 158 |
+
st.markdown("---")
|
| 159 |
+
|
| 160 |
+
# Recent updates section
|
| 161 |
+
st.markdown("### 📊 Recent Regulatory Updates")
|
| 162 |
+
|
| 163 |
+
st.markdown("View the latest regulatory changes across all jurisdictions:")
|
| 164 |
+
|
| 165 |
+
# Mock recent updates (in production, this would query the database)
|
| 166 |
+
recent_updates = [
|
| 167 |
+
{
|
| 168 |
+
"date": "2025-10-20",
|
| 169 |
+
"jurisdiction": "🇪🇺 EU",
|
| 170 |
+
"title": "MiCA: Final technical standards published",
|
| 171 |
+
"type": "New Regulation",
|
| 172 |
+
"impact": "High"
|
| 173 |
+
},
|
| 174 |
+
{
|
| 175 |
+
"date": "2025-10-18",
|
| 176 |
+
"jurisdiction": "🇺🇸 US",
|
| 177 |
+
"title": "SEC updates custody rule guidance",
|
| 178 |
+
"type": "Guidance",
|
| 179 |
+
"impact": "Medium"
|
| 180 |
+
},
|
| 181 |
+
{
|
| 182 |
+
"date": "2025-10-15",
|
| 183 |
+
"jurisdiction": "🇸🇬 Singapore",
|
| 184 |
+
"title": "MAS clarifies staking service requirements",
|
| 185 |
+
"type": "Clarification",
|
| 186 |
+
"impact": "Medium"
|
| 187 |
+
},
|
| 188 |
+
{
|
| 189 |
+
"date": "2025-10-12",
|
| 190 |
+
"jurisdiction": "🇬🇧 UK",
|
| 191 |
+
"title": "FCA financial promotions regime deadline extended",
|
| 192 |
+
"type": "Deadline Change",
|
| 193 |
+
"impact": "Low"
|
| 194 |
+
},
|
| 195 |
+
{
|
| 196 |
+
"date": "2025-10-10",
|
| 197 |
+
"jurisdiction": "🇦🇪 UAE",
|
| 198 |
+
"title": "VARA issues marketing and promotions rulebook",
|
| 199 |
+
"type": "New Regulation",
|
| 200 |
+
"impact": "High"
|
| 201 |
+
}
|
| 202 |
+
]
|
| 203 |
+
|
| 204 |
+
for update in recent_updates:
|
| 205 |
+
with st.container():
|
| 206 |
+
col1, col2, col3, col4 = st.columns([1, 3, 2, 1])
|
| 207 |
+
|
| 208 |
+
with col1:
|
| 209 |
+
st.markdown(f"**{update['date']}**")
|
| 210 |
+
|
| 211 |
+
with col2:
|
| 212 |
+
st.markdown(f"{update['jurisdiction']} • {update['title']}")
|
| 213 |
+
|
| 214 |
+
with col3:
|
| 215 |
+
st.markdown(f"*{update['type']}*")
|
| 216 |
+
|
| 217 |
+
with col4:
|
| 218 |
+
impact_colors = {
|
| 219 |
+
'High': 'red',
|
| 220 |
+
'Medium': 'orange',
|
| 221 |
+
'Low': 'green'
|
| 222 |
+
}
|
| 223 |
+
color = impact_colors.get(update['impact'], 'blue')
|
| 224 |
+
st.markdown(f":{color}[{update['impact']}]")
|
| 225 |
+
|
| 226 |
+
st.markdown("---")
|
| 227 |
+
|
| 228 |
+
# Statistics
|
| 229 |
+
st.markdown("### 📈 Update Statistics")
|
| 230 |
+
|
| 231 |
+
col1, col2, col3, col4 = st.columns(4)
|
| 232 |
+
|
| 233 |
+
with col1:
|
| 234 |
+
st.metric("Updates This Week", "12")
|
| 235 |
+
|
| 236 |
+
with col2:
|
| 237 |
+
st.metric("Active Subscribers", "247")
|
| 238 |
+
|
| 239 |
+
with col3:
|
| 240 |
+
st.metric("Avg. Updates/Month", "48")
|
| 241 |
+
|
| 242 |
+
with col4:
|
| 243 |
+
st.metric("Coverage", "5 jurisdictions")
|
| 244 |
+
|
| 245 |
+
st.markdown("---")
|
| 246 |
+
|
| 247 |
+
st.caption("💡 Tip: Enable notifications for multiple jurisdictions to stay informed about regulatory arbitrage opportunities")
|
app/components/results_display.py
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Results Display Component
|
| 3 |
+
|
| 4 |
+
Renders analysis results with visualizations and detailed breakdowns.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import streamlit as st
|
| 8 |
+
import plotly.graph_objects as go
|
| 9 |
+
from typing import Dict, Any
|
| 10 |
+
from datetime import datetime, timedelta
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def render_results(result: Dict[Any, Any], form_data: Dict[str, Any]):
|
| 14 |
+
"""
|
| 15 |
+
Render comprehensive analysis results.
|
| 16 |
+
|
| 17 |
+
Args:
|
| 18 |
+
result: Agent analysis result
|
| 19 |
+
form_data: Original form data
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
# Summary section
|
| 23 |
+
st.markdown("### 📊 Executive Summary")
|
| 24 |
+
|
| 25 |
+
col1, col2, col3, col4 = st.columns(4)
|
| 26 |
+
|
| 27 |
+
with col1:
|
| 28 |
+
risk_score = result.get('risk_score', 0)
|
| 29 |
+
if risk_score:
|
| 30 |
+
st.metric("Risk Score", f"{risk_score:.1f}/100")
|
| 31 |
+
else:
|
| 32 |
+
st.metric("Risk Score", "N/A")
|
| 33 |
+
|
| 34 |
+
with col2:
|
| 35 |
+
gaps = result.get('compliance_gaps', [])
|
| 36 |
+
st.metric("Compliance Gaps", len(gaps) if gaps else 0)
|
| 37 |
+
|
| 38 |
+
with col3:
|
| 39 |
+
cost_estimate = result.get('cost_estimate', {})
|
| 40 |
+
# Calculate total from jurisdiction costs
|
| 41 |
+
first_year_cost = 0
|
| 42 |
+
if cost_estimate:
|
| 43 |
+
for jur_cost in cost_estimate.values():
|
| 44 |
+
if isinstance(jur_cost, dict) and 'first_year' in jur_cost:
|
| 45 |
+
first_year_cost += jur_cost['first_year'].get('estimate', 0)
|
| 46 |
+
st.metric("First Year Cost", f"${first_year_cost:,.0f}" if first_year_cost > 0 else "N/A")
|
| 47 |
+
|
| 48 |
+
with col4:
|
| 49 |
+
jurisdictions = form_data.get('jurisdictions', [])
|
| 50 |
+
st.metric("Jurisdictions", len(jurisdictions))
|
| 51 |
+
|
| 52 |
+
st.markdown("---")
|
| 53 |
+
|
| 54 |
+
# Risk gauge
|
| 55 |
+
render_risk_gauge(risk_score)
|
| 56 |
+
|
| 57 |
+
st.markdown("---")
|
| 58 |
+
|
| 59 |
+
# Compliance gaps
|
| 60 |
+
if gaps:
|
| 61 |
+
render_compliance_gaps(gaps, form_data.get('jurisdictions', []))
|
| 62 |
+
else:
|
| 63 |
+
st.success("✅ No major compliance gaps identified!")
|
| 64 |
+
|
| 65 |
+
st.markdown("---")
|
| 66 |
+
|
| 67 |
+
# Token analysis (if applicable)
|
| 68 |
+
if form_data.get('issue_token') and result.get('token_classification'):
|
| 69 |
+
render_token_analysis(result['token_classification'])
|
| 70 |
+
st.markdown("---")
|
| 71 |
+
|
| 72 |
+
# Cost breakdown
|
| 73 |
+
if cost_estimate:
|
| 74 |
+
render_cost_breakdown(cost_estimate, form_data.get('jurisdictions', []))
|
| 75 |
+
st.markdown("---")
|
| 76 |
+
|
| 77 |
+
# Recommendations
|
| 78 |
+
recommendations = result.get('recommendations', [])
|
| 79 |
+
if recommendations:
|
| 80 |
+
render_recommendations(recommendations)
|
| 81 |
+
st.markdown("---")
|
| 82 |
+
|
| 83 |
+
# Agent reasoning - hidden from user view (moved to logs only)
|
| 84 |
+
# Users don't need to see internal agent workflow
|
| 85 |
+
# reasoning = result.get('reasoning', [])
|
| 86 |
+
# if reasoning:
|
| 87 |
+
# render_reasoning_chain(reasoning)
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def render_risk_gauge(risk_score: float):
|
| 91 |
+
"""Render risk score gauge chart."""
|
| 92 |
+
|
| 93 |
+
st.markdown("### 🎯 Risk Assessment")
|
| 94 |
+
|
| 95 |
+
# Determine risk level and color
|
| 96 |
+
if risk_score >= 86:
|
| 97 |
+
risk_level = "CRITICAL"
|
| 98 |
+
color = "darkred"
|
| 99 |
+
elif risk_score >= 71:
|
| 100 |
+
risk_level = "HIGH"
|
| 101 |
+
color = "red"
|
| 102 |
+
elif risk_score >= 41:
|
| 103 |
+
risk_level = "MEDIUM"
|
| 104 |
+
color = "orange"
|
| 105 |
+
else:
|
| 106 |
+
risk_level = "LOW"
|
| 107 |
+
color = "green"
|
| 108 |
+
|
| 109 |
+
# Create gauge chart
|
| 110 |
+
fig = go.Figure(go.Indicator(
|
| 111 |
+
mode="gauge+number",
|
| 112 |
+
value=risk_score,
|
| 113 |
+
domain={'x': [0, 1], 'y': [0, 1]},
|
| 114 |
+
title={'text': f"Risk Level: {risk_level}", 'font': {'size': 24}},
|
| 115 |
+
gauge={
|
| 116 |
+
'axis': {'range': [None, 100], 'tickwidth': 1, 'tickcolor': "darkblue"},
|
| 117 |
+
'bar': {'color': color},
|
| 118 |
+
'bgcolor': "white",
|
| 119 |
+
'borderwidth': 2,
|
| 120 |
+
'bordercolor': "gray",
|
| 121 |
+
'steps': [
|
| 122 |
+
{'range': [0, 40], 'color': 'lightgreen'},
|
| 123 |
+
{'range': [40, 70], 'color': 'lightyellow'},
|
| 124 |
+
{'range': [70, 85], 'color': 'lightcoral'},
|
| 125 |
+
{'range': [85, 100], 'color': 'darkred'}
|
| 126 |
+
],
|
| 127 |
+
'threshold': {
|
| 128 |
+
'line': {'color': "red", 'width': 4},
|
| 129 |
+
'thickness': 0.75,
|
| 130 |
+
'value': 85
|
| 131 |
+
}
|
| 132 |
+
}
|
| 133 |
+
))
|
| 134 |
+
|
| 135 |
+
fig.update_layout(
|
| 136 |
+
height=300,
|
| 137 |
+
margin=dict(l=20, r=20, t=60, b=20)
|
| 138 |
+
)
|
| 139 |
+
|
| 140 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 141 |
+
|
| 142 |
+
# Risk explanation
|
| 143 |
+
col1, col2 = st.columns([1, 3])
|
| 144 |
+
|
| 145 |
+
with col1:
|
| 146 |
+
if risk_level == "CRITICAL":
|
| 147 |
+
st.error("🚨 CRITICAL RISK")
|
| 148 |
+
elif risk_level == "HIGH":
|
| 149 |
+
st.warning("⚠️ HIGH RISK")
|
| 150 |
+
elif risk_level == "MEDIUM":
|
| 151 |
+
st.info("ℹ️ MEDIUM RISK")
|
| 152 |
+
else:
|
| 153 |
+
st.success("✅ LOW RISK")
|
| 154 |
+
|
| 155 |
+
with col2:
|
| 156 |
+
explanations = {
|
| 157 |
+
"CRITICAL": "Immediate action required. Multiple critical compliance gaps that could result in severe penalties or business shutdown.",
|
| 158 |
+
"HIGH": "Urgent compliance measures needed. Significant gaps that pose substantial legal and financial risk.",
|
| 159 |
+
"MEDIUM": "Compliance improvements recommended. Several gaps identified that should be addressed systematically.",
|
| 160 |
+
"LOW": "Minimal compliance concerns. Continue monitoring and address minor items as identified."
|
| 161 |
+
}
|
| 162 |
+
st.markdown(explanations[risk_level])
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
def render_compliance_gaps(gaps: list, jurisdictions: list):
|
| 166 |
+
"""Render compliance gaps table by jurisdiction."""
|
| 167 |
+
|
| 168 |
+
st.markdown("### ⚠️ Compliance Gaps")
|
| 169 |
+
|
| 170 |
+
# Agent returns gaps as simple list without jurisdiction field
|
| 171 |
+
# Display all gaps in a single section for now
|
| 172 |
+
if gaps:
|
| 173 |
+
st.markdown(f"**{len(gaps)} compliance gaps identified across all jurisdictions**")
|
| 174 |
+
|
| 175 |
+
for gap in gaps:
|
| 176 |
+
render_gap_card(gap)
|
| 177 |
+
else:
|
| 178 |
+
st.success("✅ No major compliance gaps identified!")
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
def render_gap_card(gap: Dict[str, Any]):
|
| 182 |
+
"""Render individual gap card - always expanded for immediate visibility."""
|
| 183 |
+
|
| 184 |
+
# Agent returns gaps as simple dicts with 'description' and 'severity'
|
| 185 |
+
severity = gap.get('severity', 'medium')
|
| 186 |
+
description = gap.get('description', 'Compliance requirement')
|
| 187 |
+
|
| 188 |
+
# Severity badge with icons
|
| 189 |
+
severity_config = {
|
| 190 |
+
'critical': {'color': 'red', 'icon': '🚨', 'bg': '#ffe6e6'},
|
| 191 |
+
'high': {'color': 'orange', 'icon': '⚠️', 'bg': '#fff3e0'},
|
| 192 |
+
'medium': {'color': 'blue', 'icon': 'ℹ️', 'bg': '#e3f2fd'},
|
| 193 |
+
'low': {'color': 'green', 'icon': '✓', 'bg': '#e8f5e9'}
|
| 194 |
+
}
|
| 195 |
+
config = severity_config.get(severity, severity_config['medium'])
|
| 196 |
+
|
| 197 |
+
# Parse description - Gemini returns multi-line format:
|
| 198 |
+
# Line 1: **[SEVERITY] Requirement Name (Jurisdiction) - Deadline: X, Cost: $Y**
|
| 199 |
+
# Line 2+: Description: [details]
|
| 200 |
+
lines = description.strip().split('\n', 1)
|
| 201 |
+
title_line = lines[0].strip()
|
| 202 |
+
detail_text = lines[1].strip() if len(lines) > 1 else ""
|
| 203 |
+
|
| 204 |
+
# Remove markdown bold from title if present
|
| 205 |
+
title_clean = title_line.replace('**', '').replace('*', '')
|
| 206 |
+
|
| 207 |
+
# Render as card (no expander - always visible)
|
| 208 |
+
st.markdown(f"""
|
| 209 |
+
<div style="background-color: {config['bg']}; padding: 1rem; border-left: 5px solid {config['color']}; border-radius: 5px; margin-bottom: 1rem;">
|
| 210 |
+
<div style="font-weight: bold; margin-bottom: 0.5rem; font-size: 1.1em;">
|
| 211 |
+
{config['icon']} {title_clean}
|
| 212 |
+
</div>
|
| 213 |
+
<div style="color: #666; margin-bottom: 0.5rem;">
|
| 214 |
+
<strong>Severity:</strong> <span style="color: {config['color']};">{severity.upper()}</span>
|
| 215 |
+
</div>
|
| 216 |
+
<div style="margin-top: 0.5rem; line-height: 1.6;">
|
| 217 |
+
{detail_text if detail_text else title_clean}
|
| 218 |
+
</div>
|
| 219 |
+
</div>
|
| 220 |
+
""", unsafe_allow_html=True)
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
def render_token_analysis(token_classification: Dict[str, Any]):
|
| 224 |
+
"""Render token classification analysis."""
|
| 225 |
+
|
| 226 |
+
st.markdown("### 🪙 Token Classification")
|
| 227 |
+
|
| 228 |
+
st.info("Token classification determines applicable regulatory frameworks and compliance requirements.")
|
| 229 |
+
|
| 230 |
+
# Create columns for different jurisdictions
|
| 231 |
+
for jurisdiction, classification in token_classification.items():
|
| 232 |
+
st.markdown(f"#### {get_jurisdiction_name(jurisdiction)}")
|
| 233 |
+
|
| 234 |
+
col1, col2 = st.columns([2, 1])
|
| 235 |
+
|
| 236 |
+
with col1:
|
| 237 |
+
classification_type = classification.get('classification', 'Unknown')
|
| 238 |
+
confidence = classification.get('confidence', 0) * 100
|
| 239 |
+
|
| 240 |
+
if classification_type in ['security', 'capital markets product']:
|
| 241 |
+
st.error(f"**Classification:** {classification_type.title()}")
|
| 242 |
+
st.warning("⚠️ Securities regulations apply. SEC/MAS registration may be required.")
|
| 243 |
+
else:
|
| 244 |
+
st.success(f"**Classification:** {classification_type.title()}")
|
| 245 |
+
|
| 246 |
+
st.progress(confidence / 100, text=f"Confidence: {confidence:.0f}%")
|
| 247 |
+
|
| 248 |
+
with col2:
|
| 249 |
+
# Howey Test results (if available)
|
| 250 |
+
howey = classification.get('howey_test', {})
|
| 251 |
+
if howey:
|
| 252 |
+
st.markdown("**Howey Test:**")
|
| 253 |
+
for prong, result in howey.items():
|
| 254 |
+
icon = "✅" if result else "❌"
|
| 255 |
+
st.markdown(f"{icon} {prong.replace('_', ' ').title()}")
|
| 256 |
+
|
| 257 |
+
# Implications
|
| 258 |
+
implications = classification.get('implications', [])
|
| 259 |
+
if implications:
|
| 260 |
+
st.markdown("**Regulatory Implications:**")
|
| 261 |
+
for implication in implications:
|
| 262 |
+
st.markdown(f"- {implication}")
|
| 263 |
+
|
| 264 |
+
st.markdown("---")
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
def render_cost_breakdown(cost_estimate: Dict[str, Any], jurisdictions: list):
|
| 268 |
+
"""Render cost breakdown and projections."""
|
| 269 |
+
|
| 270 |
+
st.markdown("### 💰 Cost Breakdown")
|
| 271 |
+
|
| 272 |
+
if not cost_estimate:
|
| 273 |
+
st.info("Cost estimation not available")
|
| 274 |
+
return
|
| 275 |
+
|
| 276 |
+
# Calculate grand totals from jurisdiction costs
|
| 277 |
+
total_first_year = 0
|
| 278 |
+
total_annual = 0
|
| 279 |
+
|
| 280 |
+
for jur_cost in cost_estimate.values():
|
| 281 |
+
if isinstance(jur_cost, dict):
|
| 282 |
+
if 'first_year' in jur_cost and isinstance(jur_cost['first_year'], dict):
|
| 283 |
+
total_first_year += jur_cost['first_year'].get('estimate', 0)
|
| 284 |
+
if 'annual' in jur_cost and isinstance(jur_cost['annual'], dict):
|
| 285 |
+
total_annual += jur_cost['annual'].get('estimate', 0)
|
| 286 |
+
|
| 287 |
+
# Grand totals
|
| 288 |
+
if total_first_year > 0 or total_annual > 0:
|
| 289 |
+
st.markdown("#### Total Estimated Costs")
|
| 290 |
+
|
| 291 |
+
col1, col2, col3 = st.columns(3)
|
| 292 |
+
|
| 293 |
+
with col1:
|
| 294 |
+
st.metric("First Year", f"${total_first_year:,.0f}")
|
| 295 |
+
|
| 296 |
+
with col2:
|
| 297 |
+
st.metric("Annual Ongoing", f"${total_annual:,.0f}")
|
| 298 |
+
|
| 299 |
+
with col3:
|
| 300 |
+
three_year = total_first_year + (total_annual * 2)
|
| 301 |
+
st.metric("3-Year Total", f"${three_year:,.0f}")
|
| 302 |
+
|
| 303 |
+
st.markdown("---")
|
| 304 |
+
|
| 305 |
+
# By jurisdiction
|
| 306 |
+
st.markdown("#### Cost by Jurisdiction")
|
| 307 |
+
|
| 308 |
+
for jurisdiction in jurisdictions:
|
| 309 |
+
jur_cost = cost_estimate.get(jurisdiction, {})
|
| 310 |
+
|
| 311 |
+
if jur_cost and isinstance(jur_cost, dict):
|
| 312 |
+
with st.expander(f"{get_jurisdiction_name(jurisdiction)}", expanded=True):
|
| 313 |
+
col1, col2 = st.columns(2)
|
| 314 |
+
|
| 315 |
+
with col1:
|
| 316 |
+
first_year_dict = jur_cost.get('first_year', {})
|
| 317 |
+
if isinstance(first_year_dict, dict):
|
| 318 |
+
first_year = first_year_dict.get('estimate', 0)
|
| 319 |
+
st.markdown(f"**First Year:** ${first_year:,.0f}")
|
| 320 |
+
|
| 321 |
+
with col2:
|
| 322 |
+
annual_dict = jur_cost.get('annual', {})
|
| 323 |
+
if isinstance(annual_dict, dict):
|
| 324 |
+
annual = annual_dict.get('estimate', 0)
|
| 325 |
+
st.markdown(f"**Annual:** ${annual:,.0f}")
|
| 326 |
+
|
| 327 |
+
|
| 328 |
+
def render_recommendations(recommendations: list):
|
| 329 |
+
"""Render prioritized recommendations."""
|
| 330 |
+
|
| 331 |
+
st.markdown("### 📋 Recommendations")
|
| 332 |
+
|
| 333 |
+
st.info("Prioritized action items to achieve compliance")
|
| 334 |
+
|
| 335 |
+
for i, rec in enumerate(recommendations, 1):
|
| 336 |
+
if isinstance(rec, dict):
|
| 337 |
+
title = rec.get('title', f'Recommendation {i}')
|
| 338 |
+
description = rec.get('description', '')
|
| 339 |
+
priority = rec.get('priority', 'medium')
|
| 340 |
+
timeline = rec.get('timeline', '')
|
| 341 |
+
|
| 342 |
+
priority_icons = {'high': '🔴', 'medium': '🟡', 'low': '🟢'}
|
| 343 |
+
icon = priority_icons.get(priority, '🔵')
|
| 344 |
+
|
| 345 |
+
with st.expander(f"{icon} **{i}. {title}**", expanded=(i <= 3)):
|
| 346 |
+
if description:
|
| 347 |
+
st.markdown(description)
|
| 348 |
+
if timeline:
|
| 349 |
+
st.markdown(f"**Timeline:** {timeline}")
|
| 350 |
+
else:
|
| 351 |
+
# Simple string recommendation
|
| 352 |
+
st.markdown(f"{i}. {rec}")
|
| 353 |
+
|
| 354 |
+
|
| 355 |
+
def render_reasoning_chain(reasoning: list):
|
| 356 |
+
"""Render agent reasoning chain for transparency."""
|
| 357 |
+
|
| 358 |
+
with st.expander("🧠 Agent Reasoning Chain (Explainability)", expanded=False):
|
| 359 |
+
st.markdown("The AI agent's decision-making process:")
|
| 360 |
+
|
| 361 |
+
for i, step in enumerate(reasoning, 1):
|
| 362 |
+
st.markdown(f"**Step {i}:** {step}")
|
| 363 |
+
|
| 364 |
+
|
| 365 |
+
def get_jurisdiction_name(jurisdiction: str) -> str:
|
| 366 |
+
"""Get display name for jurisdiction."""
|
| 367 |
+
mapping = {
|
| 368 |
+
'us': '🇺🇸 United States',
|
| 369 |
+
'eu': '🇪🇺 European Union',
|
| 370 |
+
'singapore': '🇸🇬 Singapore',
|
| 371 |
+
'uk': '🇬🇧 United Kingdom',
|
| 372 |
+
'uae': '🇦🇪 UAE'
|
| 373 |
+
}
|
| 374 |
+
return mapping.get(jurisdiction, jurisdiction.upper())
|
app/streamlit_app.py
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Crypto Compliance Intelligence Agent - Streamlit Web Interface
|
| 3 |
+
|
| 4 |
+
Main application entry point for the compliance analysis web app.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import streamlit as st
|
| 8 |
+
import sys
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
# Add src to path
|
| 12 |
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
| 13 |
+
|
| 14 |
+
from src.config import Config
|
| 15 |
+
from src.agents.orchestrator import ComplianceAgent
|
| 16 |
+
from app.components.input_form import render_input_form
|
| 17 |
+
from app.components.results_display import render_results
|
| 18 |
+
from app.components.monitoring_setup import render_monitoring_setup
|
| 19 |
+
|
| 20 |
+
# Page configuration
|
| 21 |
+
st.set_page_config(
|
| 22 |
+
page_title="Crypto Compliance Intelligence Agent",
|
| 23 |
+
page_icon="🔒",
|
| 24 |
+
layout="wide",
|
| 25 |
+
initial_sidebar_state="expanded"
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
# Custom CSS
|
| 29 |
+
st.markdown("""
|
| 30 |
+
<style>
|
| 31 |
+
.main-header {
|
| 32 |
+
font-size: 2.5rem;
|
| 33 |
+
font-weight: 700;
|
| 34 |
+
color: #1f77b4;
|
| 35 |
+
margin-bottom: 0.5rem;
|
| 36 |
+
}
|
| 37 |
+
.sub-header {
|
| 38 |
+
font-size: 1.2rem;
|
| 39 |
+
color: #666;
|
| 40 |
+
margin-bottom: 2rem;
|
| 41 |
+
}
|
| 42 |
+
.disclaimer-box {
|
| 43 |
+
background-color: #fff3cd;
|
| 44 |
+
border-left: 5px solid #ffc107;
|
| 45 |
+
padding: 1rem;
|
| 46 |
+
margin: 1rem 0;
|
| 47 |
+
border-radius: 0.25rem;
|
| 48 |
+
}
|
| 49 |
+
.stButton>button {
|
| 50 |
+
width: 100%;
|
| 51 |
+
background-color: #1f77b4;
|
| 52 |
+
color: white;
|
| 53 |
+
font-weight: 600;
|
| 54 |
+
padding: 0.75rem;
|
| 55 |
+
border-radius: 0.5rem;
|
| 56 |
+
}
|
| 57 |
+
.stButton>button:hover {
|
| 58 |
+
background-color: #1557a0;
|
| 59 |
+
}
|
| 60 |
+
</style>
|
| 61 |
+
""", unsafe_allow_html=True)
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
@st.cache_resource(ttl=600) # Cache for 10 minutes, then refresh
|
| 65 |
+
def initialize_agent():
|
| 66 |
+
"""Initialize the compliance agent (cached for performance)."""
|
| 67 |
+
try:
|
| 68 |
+
agent = ComplianceAgent()
|
| 69 |
+
return agent
|
| 70 |
+
except Exception as e:
|
| 71 |
+
st.error(f"Failed to initialize agent: {e}")
|
| 72 |
+
return None
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def render_header():
|
| 76 |
+
"""Render application header."""
|
| 77 |
+
st.markdown('<div class="main-header">🔒 Crypto Compliance Intelligence Agent</div>', unsafe_allow_html=True)
|
| 78 |
+
st.markdown(
|
| 79 |
+
'<div class="sub-header">Multi-jurisdiction compliance analysis powered by AI</div>',
|
| 80 |
+
unsafe_allow_html=True
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def render_disclaimer():
|
| 85 |
+
"""Render legal disclaimer."""
|
| 86 |
+
st.markdown("""
|
| 87 |
+
<div class="disclaimer-box">
|
| 88 |
+
<h4>⚠️ Important Disclaimer</h4>
|
| 89 |
+
<p>This tool provides <strong>general information only</strong> and does <strong>NOT</strong> constitute legal, financial, or regulatory advice. The analysis is based on publicly available information and AI models, which may contain errors or be outdated.</p>
|
| 90 |
+
<p><strong>Always consult qualified legal counsel</strong> before making compliance decisions. The creators assume no liability for decisions made based on this tool.</p>
|
| 91 |
+
</div>
|
| 92 |
+
""", unsafe_allow_html=True)
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def render_sidebar():
|
| 96 |
+
"""Render sidebar with information and settings."""
|
| 97 |
+
with st.sidebar:
|
| 98 |
+
st.image("https://via.placeholder.com/300x100/1f77b4/ffffff?text=Compliance+Agent", use_column_width=True)
|
| 99 |
+
|
| 100 |
+
st.markdown("### About")
|
| 101 |
+
st.markdown("""
|
| 102 |
+
This AI-powered system analyzes crypto compliance requirements across:
|
| 103 |
+
- 🇺🇸 United States (SEC)
|
| 104 |
+
- 🇪🇺 European Union (MiCA)
|
| 105 |
+
- 🇸🇬 Singapore (MAS)
|
| 106 |
+
- 🇬🇧 United Kingdom (FCA)
|
| 107 |
+
- 🇦🇪 UAE (VARA)
|
| 108 |
+
""")
|
| 109 |
+
|
| 110 |
+
st.markdown("### Features")
|
| 111 |
+
st.markdown("""
|
| 112 |
+
✅ Multi-jurisdiction analysis
|
| 113 |
+
✅ Token classification (Howey Test)
|
| 114 |
+
✅ Risk scoring (0-100)
|
| 115 |
+
✅ Cost estimation
|
| 116 |
+
✅ Compliance gap identification
|
| 117 |
+
✅ Actionable recommendations
|
| 118 |
+
""")
|
| 119 |
+
|
| 120 |
+
st.markdown("### How It Works")
|
| 121 |
+
st.markdown("""
|
| 122 |
+
1. Describe your business and activities
|
| 123 |
+
2. Select target jurisdictions
|
| 124 |
+
3. AI agent analyzes compliance requirements
|
| 125 |
+
4. Receive comprehensive report (30-60s)
|
| 126 |
+
""")
|
| 127 |
+
|
| 128 |
+
st.markdown("---")
|
| 129 |
+
st.markdown("**Powered by:**")
|
| 130 |
+
st.markdown("- Google Gemini Flash 2.5")
|
| 131 |
+
st.markdown("- LangGraph Agents")
|
| 132 |
+
st.markdown("- ChromaDB Vector Search")
|
| 133 |
+
|
| 134 |
+
st.markdown("---")
|
| 135 |
+
st.caption("Version 1.0 | Phase 6")
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
def main():
|
| 139 |
+
"""Main application logic."""
|
| 140 |
+
|
| 141 |
+
# Render header and disclaimer
|
| 142 |
+
render_header()
|
| 143 |
+
render_disclaimer()
|
| 144 |
+
|
| 145 |
+
# Render sidebar
|
| 146 |
+
render_sidebar()
|
| 147 |
+
|
| 148 |
+
# Initialize agent
|
| 149 |
+
agent = initialize_agent()
|
| 150 |
+
|
| 151 |
+
if agent is None:
|
| 152 |
+
st.error("Failed to initialize compliance agent. Please check your configuration and API keys.")
|
| 153 |
+
st.stop()
|
| 154 |
+
|
| 155 |
+
# Create tabs
|
| 156 |
+
tab1, tab2, tab3 = st.tabs(["📝 Analysis", "📊 Results", "🔔 Monitoring"])
|
| 157 |
+
|
| 158 |
+
with tab1:
|
| 159 |
+
st.markdown("## Compliance Analysis")
|
| 160 |
+
st.markdown("Provide information about your crypto business to get a comprehensive compliance analysis.")
|
| 161 |
+
|
| 162 |
+
# Render input form
|
| 163 |
+
form_data = render_input_form()
|
| 164 |
+
|
| 165 |
+
if form_data:
|
| 166 |
+
# User submitted the form
|
| 167 |
+
st.markdown("---")
|
| 168 |
+
|
| 169 |
+
with st.spinner("🤖 AI Agent is analyzing your compliance requirements... This may take 30-60 seconds."):
|
| 170 |
+
try:
|
| 171 |
+
# Run agent analysis
|
| 172 |
+
result = agent.run(
|
| 173 |
+
user_input=form_data['business_description'],
|
| 174 |
+
jurisdictions=form_data['jurisdictions'],
|
| 175 |
+
activities=form_data['activities'],
|
| 176 |
+
token_description=form_data.get('token_description')
|
| 177 |
+
)
|
| 178 |
+
|
| 179 |
+
# Store result in session state
|
| 180 |
+
st.session_state['analysis_result'] = result
|
| 181 |
+
st.session_state['form_data'] = form_data
|
| 182 |
+
|
| 183 |
+
st.success("✅ Analysis complete! View results in the **Results** tab.")
|
| 184 |
+
|
| 185 |
+
except Exception as e:
|
| 186 |
+
st.error(f"❌ Analysis failed: {str(e)}")
|
| 187 |
+
st.exception(e)
|
| 188 |
+
|
| 189 |
+
with tab2:
|
| 190 |
+
st.markdown("## Analysis Results")
|
| 191 |
+
|
| 192 |
+
if 'analysis_result' in st.session_state:
|
| 193 |
+
result = st.session_state['analysis_result']
|
| 194 |
+
form_data = st.session_state.get('form_data', {})
|
| 195 |
+
|
| 196 |
+
render_results(result, form_data)
|
| 197 |
+
else:
|
| 198 |
+
st.info("👈 Complete the analysis in the **Analysis** tab to see results here.")
|
| 199 |
+
|
| 200 |
+
with tab3:
|
| 201 |
+
st.markdown("## Compliance Monitoring")
|
| 202 |
+
st.markdown("Set up alerts to stay informed about regulatory changes.")
|
| 203 |
+
|
| 204 |
+
render_monitoring_setup()
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
if __name__ == "__main__":
|
| 208 |
+
main()
|
chroma_db/c30b23e7-6559-433a-b87d-7290f0978053/data_level0.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:571c95cfab76b4001dd6f0a0510f8b5741632272159d49f94759e4a66493ed90
|
| 3 |
+
size 1676000
|
chroma_db/c30b23e7-6559-433a-b87d-7290f0978053/header.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e87a1dc8bcae6f2c4bea6d5dd5005454d4dace8637dae29bff3c037ea771411e
|
| 3 |
+
size 100
|
chroma_db/c30b23e7-6559-433a-b87d-7290f0978053/length.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:fc19b1997119425765295aeab72d76faa6927d4f83985d328c26f20468d6cc76
|
| 3 |
+
size 4000
|
chroma_db/c30b23e7-6559-433a-b87d-7290f0978053/link_lists.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
|
| 3 |
+
size 0
|
chroma_db/chroma.sqlite3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:dd2c9e8fc0f11da0446bdf60f58c0bd05fbc048dc036cb5717c62d9ed9f10ce5
|
| 3 |
+
size 819200
|
config.toml
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[theme]
|
| 2 |
+
primaryColor = "#1f77b4"
|
| 3 |
+
backgroundColor = "#FFFFFF"
|
| 4 |
+
secondaryBackgroundColor = "#F0F2F6"
|
| 5 |
+
textColor = "#262730"
|
| 6 |
+
font = "sans serif"
|
| 7 |
+
|
| 8 |
+
[server]
|
| 9 |
+
headless = true
|
| 10 |
+
port = 7860
|
| 11 |
+
enableCORS = false
|
| 12 |
+
enableXsrfProtection = true
|
| 13 |
+
|
| 14 |
+
[browser]
|
| 15 |
+
gatherUsageStats = false
|
requirements.txt
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Web Framework
|
| 2 |
+
streamlit==1.29.0
|
| 3 |
+
|
| 4 |
+
# LLM and Agent Framework
|
| 5 |
+
langchain>=0.1.0
|
| 6 |
+
langgraph>=0.0.20
|
| 7 |
+
langchain-google-genai>=1.0.0
|
| 8 |
+
google-generativeai>=0.4.0
|
| 9 |
+
|
| 10 |
+
# Vector Database and Embeddings (lightweight)
|
| 11 |
+
chromadb==0.4.22
|
| 12 |
+
sentence-transformers==2.3.1
|
| 13 |
+
|
| 14 |
+
# Data Processing (minimal)
|
| 15 |
+
pandas>=2.2.0
|
| 16 |
+
numpy>=1.26.0
|
| 17 |
+
pdfplumber>=0.10.0
|
| 18 |
+
beautifulsoup4>=4.12.0
|
| 19 |
+
lxml>=5.1.0
|
| 20 |
+
|
| 21 |
+
# HTTP and Web Scraping
|
| 22 |
+
requests==2.31.0
|
| 23 |
+
|
| 24 |
+
# Utilities
|
| 25 |
+
python-dotenv==1.0.0
|
| 26 |
+
python-dateutil==2.8.2
|
| 27 |
+
|
| 28 |
+
# Plotting
|
| 29 |
+
plotly>=5.18.0
|
| 30 |
+
|
| 31 |
+
# Note: FinBERT and Legal-BERT accessed via HuggingFace Inference API
|
| 32 |
+
# No need to download torch/transformers locally - saves storage!
|
src/__init__.py
ADDED
|
File without changes
|
src/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (191 Bytes). View file
|
|
|
src/__pycache__/config.cpython-313.pyc
ADDED
|
Binary file (3.17 kB). View file
|
|
|
src/agents/__init__.py
ADDED
|
File without changes
|
src/agents/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (198 Bytes). View file
|
|
|
src/agents/__pycache__/orchestrator.cpython-313.pyc
ADDED
|
Binary file (27.2 kB). View file
|
|
|
src/agents/__pycache__/tools.cpython-313.pyc
ADDED
|
Binary file (15 kB). View file
|
|
|
src/agents/orchestrator.py
ADDED
|
@@ -0,0 +1,621 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
LangGraph orchestrator for compliance analysis workflow.
|
| 3 |
+
Implements a state machine with 6 nodes for end-to-end analysis.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import logging
|
| 7 |
+
from typing import Dict, List, Optional, TypedDict, Annotated
|
| 8 |
+
from datetime import datetime
|
| 9 |
+
import operator
|
| 10 |
+
|
| 11 |
+
from langgraph.graph import StateGraph, END
|
| 12 |
+
from langchain_google_genai import ChatGoogleGenerativeAI
|
| 13 |
+
|
| 14 |
+
from src.config import Config
|
| 15 |
+
from src.agents.tools import (
|
| 16 |
+
search_regulations,
|
| 17 |
+
calculate_compliance_cost,
|
| 18 |
+
analyze_token_security,
|
| 19 |
+
extract_entities_from_text
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
logger = logging.getLogger(__name__)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
# Agent State Definition
|
| 26 |
+
class ComplianceState(TypedDict):
|
| 27 |
+
"""State that flows through the agent workflow."""
|
| 28 |
+
# Input
|
| 29 |
+
user_input: str
|
| 30 |
+
jurisdictions: List[str]
|
| 31 |
+
activities: List[str]
|
| 32 |
+
token_description: Optional[str]
|
| 33 |
+
|
| 34 |
+
# Intermediate results
|
| 35 |
+
business_type: Optional[str]
|
| 36 |
+
extracted_entities: Optional[Dict]
|
| 37 |
+
token_classification: Optional[Dict]
|
| 38 |
+
relevant_regulations: Optional[List[Dict]]
|
| 39 |
+
compliance_gaps: Optional[List[Dict]]
|
| 40 |
+
risk_score: Optional[float]
|
| 41 |
+
cost_estimate: Optional[Dict]
|
| 42 |
+
|
| 43 |
+
# Output
|
| 44 |
+
recommendations: Optional[List[str]]
|
| 45 |
+
reasoning: Annotated[List[str], operator.add] # Accumulate reasoning steps
|
| 46 |
+
|
| 47 |
+
# Metadata
|
| 48 |
+
timestamp: str
|
| 49 |
+
errors: Annotated[List[str], operator.add]
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
class ComplianceAgent:
|
| 53 |
+
"""
|
| 54 |
+
LangGraph-based compliance agent orchestrator.
|
| 55 |
+
|
| 56 |
+
Workflow:
|
| 57 |
+
1. classify_input -> Determine business model type
|
| 58 |
+
2. extract_entities -> Extract key information from description
|
| 59 |
+
3. retrieve_regulations -> Search vector DB for relevant rules
|
| 60 |
+
4. analyze_compliance -> Identify gaps and requirements
|
| 61 |
+
5. calculate_risk -> Generate risk score
|
| 62 |
+
6. generate_recommendations -> Create actionable steps
|
| 63 |
+
"""
|
| 64 |
+
|
| 65 |
+
def __init__(self):
|
| 66 |
+
"""Initialize the compliance agent."""
|
| 67 |
+
# Initialize LLM
|
| 68 |
+
self.llm = ChatGoogleGenerativeAI(
|
| 69 |
+
model=Config.GEMINI_MODEL,
|
| 70 |
+
google_api_key=Config.GEMINI_API_KEY,
|
| 71 |
+
temperature=Config.GEMINI_TEMPERATURE,
|
| 72 |
+
max_tokens=Config.GEMINI_MAX_TOKENS
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
# Initialize VectorDB (once, reused for all searches)
|
| 76 |
+
from src.data.vectordb import RegulatoryVectorDB
|
| 77 |
+
self.vectordb = RegulatoryVectorDB()
|
| 78 |
+
logger.info(f"VectorDB initialized with collection: {self.vectordb.collection_name}")
|
| 79 |
+
|
| 80 |
+
# Build workflow graph
|
| 81 |
+
self.workflow = self._build_workflow()
|
| 82 |
+
self.app = self.workflow.compile()
|
| 83 |
+
|
| 84 |
+
logger.info("ComplianceAgent initialized with LangGraph workflow")
|
| 85 |
+
|
| 86 |
+
def _build_workflow(self) -> StateGraph:
|
| 87 |
+
"""Build the LangGraph state machine workflow."""
|
| 88 |
+
workflow = StateGraph(ComplianceState)
|
| 89 |
+
|
| 90 |
+
# Add nodes
|
| 91 |
+
workflow.add_node("classify_input", self.classify_input_node)
|
| 92 |
+
workflow.add_node("extract_entities", self.extract_entities_node)
|
| 93 |
+
workflow.add_node("retrieve_regulations", self.retrieve_regulations_node)
|
| 94 |
+
workflow.add_node("analyze_compliance", self.analyze_compliance_node)
|
| 95 |
+
workflow.add_node("calculate_risk", self.calculate_risk_node)
|
| 96 |
+
workflow.add_node("generate_recommendations", self.generate_recommendations_node)
|
| 97 |
+
|
| 98 |
+
# Define edges
|
| 99 |
+
workflow.set_entry_point("classify_input")
|
| 100 |
+
workflow.add_edge("classify_input", "extract_entities")
|
| 101 |
+
workflow.add_edge("extract_entities", "retrieve_regulations")
|
| 102 |
+
workflow.add_edge("retrieve_regulations", "analyze_compliance")
|
| 103 |
+
workflow.add_edge("analyze_compliance", "calculate_risk")
|
| 104 |
+
workflow.add_edge("calculate_risk", "generate_recommendations")
|
| 105 |
+
workflow.add_edge("generate_recommendations", END)
|
| 106 |
+
|
| 107 |
+
return workflow
|
| 108 |
+
|
| 109 |
+
def classify_input_node(self, state: ComplianceState) -> ComplianceState:
|
| 110 |
+
"""
|
| 111 |
+
Node 1: Classify the business model type from user input.
|
| 112 |
+
|
| 113 |
+
Args:
|
| 114 |
+
state: Current agent state
|
| 115 |
+
|
| 116 |
+
Returns:
|
| 117 |
+
Updated state with business_type
|
| 118 |
+
"""
|
| 119 |
+
logger.info("Node 1: Classifying business model...")
|
| 120 |
+
|
| 121 |
+
try:
|
| 122 |
+
prompt = f"""
|
| 123 |
+
Analyze this crypto business description and classify the business model type.
|
| 124 |
+
|
| 125 |
+
Description: {state['user_input']}
|
| 126 |
+
Activities: {', '.join(state['activities'])}
|
| 127 |
+
|
| 128 |
+
Classify into ONE of these categories:
|
| 129 |
+
- Exchange/Trading Platform
|
| 130 |
+
- Custody/Wallet Service
|
| 131 |
+
- DeFi Protocol (lending, staking, yield)
|
| 132 |
+
- Token/NFT Platform
|
| 133 |
+
- Payment Processor
|
| 134 |
+
- Mining/Validator Service
|
| 135 |
+
- Other (specify)
|
| 136 |
+
|
| 137 |
+
Respond with just the category name.
|
| 138 |
+
"""
|
| 139 |
+
|
| 140 |
+
response = self.llm.invoke(prompt)
|
| 141 |
+
business_type = response.content.strip()
|
| 142 |
+
|
| 143 |
+
state['business_type'] = business_type
|
| 144 |
+
state['reasoning'].append(f"Classified business as: {business_type}")
|
| 145 |
+
|
| 146 |
+
logger.info(f"Business classified as: {business_type}")
|
| 147 |
+
|
| 148 |
+
except Exception as e:
|
| 149 |
+
logger.error(f"Error in classify_input_node: {e}")
|
| 150 |
+
state['errors'].append(f"Classification error: {str(e)}")
|
| 151 |
+
state['business_type'] = "Unknown"
|
| 152 |
+
|
| 153 |
+
return state
|
| 154 |
+
|
| 155 |
+
def extract_entities_node(self, state: ComplianceState) -> ComplianceState:
|
| 156 |
+
"""
|
| 157 |
+
Node 2: Extract entities and classify token if applicable.
|
| 158 |
+
|
| 159 |
+
Args:
|
| 160 |
+
state: Current agent state
|
| 161 |
+
|
| 162 |
+
Returns:
|
| 163 |
+
Updated state with extracted_entities and token_classification
|
| 164 |
+
"""
|
| 165 |
+
logger.info("Node 2: Extracting entities...")
|
| 166 |
+
|
| 167 |
+
try:
|
| 168 |
+
# Extract entities from user input
|
| 169 |
+
entities = extract_entities_from_text.invoke({"text": state['user_input']})
|
| 170 |
+
state['extracted_entities'] = entities
|
| 171 |
+
|
| 172 |
+
reasoning = f"Extracted entities: {entities['summary'].get('total_entities', 0)} total"
|
| 173 |
+
state['reasoning'].append(reasoning)
|
| 174 |
+
|
| 175 |
+
# Classify token if description provided
|
| 176 |
+
if state.get('token_description'):
|
| 177 |
+
# Classify in each requested jurisdiction
|
| 178 |
+
classifications = {}
|
| 179 |
+
for jurisdiction in state['jurisdictions']:
|
| 180 |
+
if jurisdiction in ['us', 'eu', 'singapore']:
|
| 181 |
+
result = analyze_token_security.invoke({
|
| 182 |
+
"token_description": state['token_description'],
|
| 183 |
+
"jurisdiction": jurisdiction
|
| 184 |
+
})
|
| 185 |
+
classifications[jurisdiction] = result
|
| 186 |
+
|
| 187 |
+
state['token_classification'] = classifications
|
| 188 |
+
|
| 189 |
+
# Check if token is security in any jurisdiction
|
| 190 |
+
is_security_anywhere = any(
|
| 191 |
+
c.get('classification') == 'security' or
|
| 192 |
+
c.get('classification') == 'capital markets product'
|
| 193 |
+
for c in classifications.values()
|
| 194 |
+
)
|
| 195 |
+
|
| 196 |
+
reasoning = f"Token classified - Security: {is_security_anywhere}"
|
| 197 |
+
state['reasoning'].append(reasoning)
|
| 198 |
+
|
| 199 |
+
logger.info(f"Token classified - Security anywhere: {is_security_anywhere}")
|
| 200 |
+
|
| 201 |
+
except Exception as e:
|
| 202 |
+
logger.error(f"Error in extract_entities_node: {e}")
|
| 203 |
+
state['errors'].append(f"Entity extraction error: {str(e)}")
|
| 204 |
+
|
| 205 |
+
return state
|
| 206 |
+
|
| 207 |
+
def retrieve_regulations_node(self, state: ComplianceState) -> ComplianceState:
|
| 208 |
+
"""
|
| 209 |
+
Node 3: Retrieve relevant regulations from vector database.
|
| 210 |
+
|
| 211 |
+
Args:
|
| 212 |
+
state: Current agent state
|
| 213 |
+
|
| 214 |
+
Returns:
|
| 215 |
+
Updated state with relevant_regulations
|
| 216 |
+
"""
|
| 217 |
+
logger.info("Node 3: Retrieving regulations...")
|
| 218 |
+
|
| 219 |
+
try:
|
| 220 |
+
all_regulations = []
|
| 221 |
+
|
| 222 |
+
# Build search query from activities and entities
|
| 223 |
+
activities_str = ', '.join(state['activities'])
|
| 224 |
+
query = f"{activities_str} compliance requirements regulations"
|
| 225 |
+
|
| 226 |
+
# Search each jurisdiction using agent's VectorDB instance
|
| 227 |
+
for jurisdiction in state['jurisdictions']:
|
| 228 |
+
results = self.vectordb.search_relevant_regulations(
|
| 229 |
+
query=query,
|
| 230 |
+
jurisdiction=jurisdiction,
|
| 231 |
+
top_k=5
|
| 232 |
+
)
|
| 233 |
+
|
| 234 |
+
logger.info(f"VectorDB search ({jurisdiction}): {len(results)} results")
|
| 235 |
+
all_regulations.extend(results)
|
| 236 |
+
|
| 237 |
+
state['relevant_regulations'] = all_regulations
|
| 238 |
+
|
| 239 |
+
reasoning = f"Retrieved {len(all_regulations)} relevant regulations across {len(state['jurisdictions'])} jurisdictions"
|
| 240 |
+
state['reasoning'].append(reasoning)
|
| 241 |
+
|
| 242 |
+
logger.info(f"Retrieved {len(all_regulations)} regulations total")
|
| 243 |
+
|
| 244 |
+
except Exception as e:
|
| 245 |
+
logger.error(f"Error in retrieve_regulations_node: {e}")
|
| 246 |
+
state['errors'].append(f"Regulation retrieval error: {str(e)}")
|
| 247 |
+
state['relevant_regulations'] = []
|
| 248 |
+
|
| 249 |
+
return state
|
| 250 |
+
|
| 251 |
+
def analyze_compliance_node(self, state: ComplianceState) -> ComplianceState:
|
| 252 |
+
"""
|
| 253 |
+
Node 4: Analyze compliance gaps by matching activities to regulations.
|
| 254 |
+
|
| 255 |
+
Args:
|
| 256 |
+
state: Current agent state
|
| 257 |
+
|
| 258 |
+
Returns:
|
| 259 |
+
Updated state with compliance_gaps
|
| 260 |
+
"""
|
| 261 |
+
logger.info("Node 4: Analyzing compliance...")
|
| 262 |
+
|
| 263 |
+
try:
|
| 264 |
+
# Build detailed regulations context with specific requirements
|
| 265 |
+
regulations_detail = []
|
| 266 |
+
for reg in state.get('relevant_regulations', [])[:5]: # Top 5 most relevant
|
| 267 |
+
reg_text = f"\n**{reg.get('title', 'Unknown')} ({reg.get('jurisdiction', '').upper()})**\n"
|
| 268 |
+
reg_text += f"Summary: {reg.get('summary', '')}\n"
|
| 269 |
+
|
| 270 |
+
# Include specific requirements from regulation
|
| 271 |
+
requirements = reg.get('requirements', [])
|
| 272 |
+
if requirements:
|
| 273 |
+
reg_text += f"\nKey Requirements ({len(requirements)} total):\n"
|
| 274 |
+
for i, req in enumerate(requirements[:8], 1): # Top 8 requirements per regulation
|
| 275 |
+
req_name = req.get('requirement', 'Unknown requirement')
|
| 276 |
+
req_desc = req.get('description', '')[:200]
|
| 277 |
+
severity = req.get('severity', 'medium')
|
| 278 |
+
cost = req.get('estimated_cost_usd', {})
|
| 279 |
+
cost_range = f"${cost.get('min', 0):,.0f}-${cost.get('max', 0):,.0f}" if cost else "N/A"
|
| 280 |
+
deadline = req.get('deadline_days', 'N/A')
|
| 281 |
+
|
| 282 |
+
reg_text += f"{i}. {req_name} [{severity.upper()}]\n"
|
| 283 |
+
reg_text += f" Description: {req_desc}...\n"
|
| 284 |
+
reg_text += f" Cost: {cost_range} | Deadline: {deadline} days\n"
|
| 285 |
+
|
| 286 |
+
regulations_detail.append(reg_text)
|
| 287 |
+
|
| 288 |
+
regulations_context = "\n".join(regulations_detail)
|
| 289 |
+
|
| 290 |
+
# Get token classification for relevant jurisdiction
|
| 291 |
+
token_class = "N/A"
|
| 292 |
+
if state.get('token_classification'):
|
| 293 |
+
# Get first jurisdiction's classification
|
| 294 |
+
first_jur = state['jurisdictions'][0] if state['jurisdictions'] else None
|
| 295 |
+
if first_jur and first_jur in state['token_classification']:
|
| 296 |
+
token_class = state['token_classification'][first_jur].get('classification', 'N/A')
|
| 297 |
+
|
| 298 |
+
prompt = f"""
|
| 299 |
+
You are a crypto compliance expert. Analyze compliance gaps for this specific business:
|
| 300 |
+
|
| 301 |
+
**Business Details:**
|
| 302 |
+
- Business Type: {state.get('business_type', 'Unknown')}
|
| 303 |
+
- Activities: {', '.join(state['activities'])}
|
| 304 |
+
- Jurisdictions: {', '.join(state['jurisdictions'])}
|
| 305 |
+
- Description: {state['user_input'][:300]}
|
| 306 |
+
- Token Classification: {token_class}
|
| 307 |
+
|
| 308 |
+
**Relevant Regulatory Requirements:**
|
| 309 |
+
{regulations_context}
|
| 310 |
+
|
| 311 |
+
**Task:**
|
| 312 |
+
Based on the SPECIFIC requirements listed above, identify compliance gaps for this business.
|
| 313 |
+
For each gap, provide:
|
| 314 |
+
1. The EXACT requirement name from the regulations above
|
| 315 |
+
2. Specific details (costs, deadlines, severity) from the regulation
|
| 316 |
+
3. Why this applies to THIS specific business model
|
| 317 |
+
|
| 318 |
+
Be SPECIFIC - reference actual requirement names, costs, and timelines from the regulations provided.
|
| 319 |
+
Do NOT make up generic requirements.
|
| 320 |
+
|
| 321 |
+
Format each gap as:
|
| 322 |
+
[SEVERITY] Requirement Name (Jurisdiction) - Deadline: X days, Cost: $X-Y
|
| 323 |
+
Description: [Why this applies + key details from regulation]
|
| 324 |
+
|
| 325 |
+
Provide 5-10 most critical gaps.
|
| 326 |
+
"""
|
| 327 |
+
|
| 328 |
+
response = self.llm.invoke(prompt)
|
| 329 |
+
gaps_text = response.content.strip()
|
| 330 |
+
|
| 331 |
+
# DEBUG: Log the raw response
|
| 332 |
+
logger.debug(f"LLM Response (first 500 chars): {gaps_text[:500]}")
|
| 333 |
+
|
| 334 |
+
# Parse into structured format - more flexible parsing
|
| 335 |
+
gaps = []
|
| 336 |
+
lines = gaps_text.split('\n')
|
| 337 |
+
|
| 338 |
+
for line in lines:
|
| 339 |
+
line = line.strip()
|
| 340 |
+
# Skip empty lines
|
| 341 |
+
if not line:
|
| 342 |
+
continue
|
| 343 |
+
|
| 344 |
+
# Match lines that look like requirements (numbered, bulleted, or bracketed)
|
| 345 |
+
if (line[0].isdigit() and '. ' in line[:5]) or \
|
| 346 |
+
line.startswith('[') or \
|
| 347 |
+
line.startswith('-') or \
|
| 348 |
+
line.startswith('*') or \
|
| 349 |
+
('CRITICAL' in line.upper() or 'HIGH' in line.upper() or 'MEDIUM' in line.upper()):
|
| 350 |
+
gaps.append({
|
| 351 |
+
'description': line,
|
| 352 |
+
'severity': self._extract_severity(line)
|
| 353 |
+
})
|
| 354 |
+
|
| 355 |
+
# If no gaps parsed but we have content, add full response as single gap for debugging
|
| 356 |
+
if not gaps and gaps_text:
|
| 357 |
+
logger.warning(f"Failed to parse gaps from LLM response. Adding full response as single gap.")
|
| 358 |
+
gaps.append({
|
| 359 |
+
'description': gaps_text[:500],
|
| 360 |
+
'severity': 'medium'
|
| 361 |
+
})
|
| 362 |
+
|
| 363 |
+
state['compliance_gaps'] = gaps
|
| 364 |
+
state['reasoning'].append(f"Identified {len(gaps)} compliance gaps from {len(regulations_detail)} regulations")
|
| 365 |
+
|
| 366 |
+
logger.info(f"Identified {len(gaps)} compliance gaps")
|
| 367 |
+
|
| 368 |
+
except Exception as e:
|
| 369 |
+
logger.error(f"Error in analyze_compliance_node: {e}")
|
| 370 |
+
state['errors'].append(f"Compliance analysis error: {str(e)}")
|
| 371 |
+
state['compliance_gaps'] = []
|
| 372 |
+
|
| 373 |
+
return state
|
| 374 |
+
|
| 375 |
+
def calculate_risk_node(self, state: ComplianceState) -> ComplianceState:
|
| 376 |
+
"""
|
| 377 |
+
Node 5: Calculate overall risk score and cost estimates.
|
| 378 |
+
|
| 379 |
+
Args:
|
| 380 |
+
state: Current agent state
|
| 381 |
+
|
| 382 |
+
Returns:
|
| 383 |
+
Updated state with risk_score and cost_estimate
|
| 384 |
+
"""
|
| 385 |
+
logger.info("Node 5: Calculating risk and costs...")
|
| 386 |
+
|
| 387 |
+
try:
|
| 388 |
+
# Calculate risk score based on gaps
|
| 389 |
+
gaps = state.get('compliance_gaps', [])
|
| 390 |
+
gap_count = len(gaps)
|
| 391 |
+
|
| 392 |
+
# Simple risk scoring (0-100)
|
| 393 |
+
# Base score from gap count
|
| 394 |
+
risk_from_gaps = min(gap_count * 15, 60)
|
| 395 |
+
|
| 396 |
+
# Add risk for security token
|
| 397 |
+
risk_from_token = 0
|
| 398 |
+
if state.get('token_classification'):
|
| 399 |
+
classifications = state['token_classification']
|
| 400 |
+
if any(c.get('classification') == 'security' for c in classifications.values()):
|
| 401 |
+
risk_from_token = 25
|
| 402 |
+
|
| 403 |
+
# Add risk for severity
|
| 404 |
+
risk_from_severity = 0
|
| 405 |
+
critical_gaps = sum(1 for g in gaps if 'critical' in g.get('description', '').lower())
|
| 406 |
+
risk_from_severity = min(critical_gaps * 5, 15)
|
| 407 |
+
|
| 408 |
+
total_risk = min(risk_from_gaps + risk_from_token + risk_from_severity, 100)
|
| 409 |
+
|
| 410 |
+
state['risk_score'] = total_risk
|
| 411 |
+
state['reasoning'].append(f"Risk score: {total_risk}/100")
|
| 412 |
+
|
| 413 |
+
# Calculate costs
|
| 414 |
+
token_class = state.get('token_classification')
|
| 415 |
+
is_security = any(
|
| 416 |
+
c.get('classification') == 'security'
|
| 417 |
+
for c in token_class.values()
|
| 418 |
+
) if token_class else False
|
| 419 |
+
|
| 420 |
+
costs = calculate_compliance_cost.invoke({
|
| 421 |
+
"jurisdictions": state['jurisdictions'],
|
| 422 |
+
"activities": state['activities'],
|
| 423 |
+
"is_security_token": is_security
|
| 424 |
+
})
|
| 425 |
+
|
| 426 |
+
state['cost_estimate'] = costs
|
| 427 |
+
|
| 428 |
+
total_first_year = sum(
|
| 429 |
+
c['first_year']['estimate']
|
| 430 |
+
for c in costs.values()
|
| 431 |
+
)
|
| 432 |
+
|
| 433 |
+
state['reasoning'].append(f"Estimated first-year cost: ${total_first_year:,}")
|
| 434 |
+
|
| 435 |
+
logger.info(f"Risk score: {total_risk}, Est. cost: ${total_first_year:,}")
|
| 436 |
+
|
| 437 |
+
except Exception as e:
|
| 438 |
+
logger.error(f"Error in calculate_risk_node: {e}")
|
| 439 |
+
state['errors'].append(f"Risk calculation error: {str(e)}")
|
| 440 |
+
state['risk_score'] = 50.0 # Default medium risk
|
| 441 |
+
|
| 442 |
+
return state
|
| 443 |
+
|
| 444 |
+
def generate_recommendations_node(self, state: ComplianceState) -> ComplianceState:
|
| 445 |
+
"""
|
| 446 |
+
Node 6: Generate actionable recommendations.
|
| 447 |
+
|
| 448 |
+
Args:
|
| 449 |
+
state: Current agent state
|
| 450 |
+
|
| 451 |
+
Returns:
|
| 452 |
+
Updated state with recommendations
|
| 453 |
+
"""
|
| 454 |
+
logger.info("Node 6: Generating recommendations...")
|
| 455 |
+
|
| 456 |
+
try:
|
| 457 |
+
# Use LLM to generate recommendations
|
| 458 |
+
gaps_summary = "\n".join([
|
| 459 |
+
f"- {gap['description']}"
|
| 460 |
+
for gap in state.get('compliance_gaps', [])[:10]
|
| 461 |
+
])
|
| 462 |
+
|
| 463 |
+
prompt = f"""
|
| 464 |
+
Generate prioritized compliance recommendations for this crypto business:
|
| 465 |
+
|
| 466 |
+
Business Type: {state.get('business_type', 'Unknown')}
|
| 467 |
+
Risk Score: {state.get('risk_score', 'Unknown')}/100
|
| 468 |
+
Jurisdictions: {', '.join(state['jurisdictions'])}
|
| 469 |
+
|
| 470 |
+
Compliance Gaps:
|
| 471 |
+
{gaps_summary}
|
| 472 |
+
|
| 473 |
+
Provide 5-8 prioritized, actionable recommendations. Each should:
|
| 474 |
+
1. Be specific and actionable
|
| 475 |
+
2. Include estimated timeline
|
| 476 |
+
3. Indicate priority (P0/P1/P2)
|
| 477 |
+
|
| 478 |
+
Format as numbered list with priority labels.
|
| 479 |
+
"""
|
| 480 |
+
|
| 481 |
+
response = self.llm.invoke(prompt)
|
| 482 |
+
recommendations_text = response.content.strip()
|
| 483 |
+
|
| 484 |
+
# Parse into list
|
| 485 |
+
recommendations = [
|
| 486 |
+
line.strip()
|
| 487 |
+
for line in recommendations_text.split('\n')
|
| 488 |
+
if line.strip() and (line[0].isdigit() or line.startswith('-') or line.startswith('P'))
|
| 489 |
+
]
|
| 490 |
+
|
| 491 |
+
state['recommendations'] = recommendations
|
| 492 |
+
state['reasoning'].append(f"Generated {len(recommendations)} recommendations")
|
| 493 |
+
|
| 494 |
+
logger.info(f"Generated {len(recommendations)} recommendations")
|
| 495 |
+
|
| 496 |
+
except Exception as e:
|
| 497 |
+
logger.error(f"Error in generate_recommendations_node: {e}")
|
| 498 |
+
state['errors'].append(f"Recommendation generation error: {str(e)}")
|
| 499 |
+
state['recommendations'] = ["Consult with legal counsel for compliance guidance"]
|
| 500 |
+
|
| 501 |
+
return state
|
| 502 |
+
|
| 503 |
+
def _extract_severity(self, text: str) -> str:
|
| 504 |
+
"""Extract severity level from text."""
|
| 505 |
+
text_lower = text.lower()
|
| 506 |
+
if 'critical' in text_lower:
|
| 507 |
+
return 'critical'
|
| 508 |
+
elif 'high' in text_lower:
|
| 509 |
+
return 'high'
|
| 510 |
+
elif 'medium' in text_lower:
|
| 511 |
+
return 'medium'
|
| 512 |
+
elif 'low' in text_lower:
|
| 513 |
+
return 'low'
|
| 514 |
+
return 'medium'
|
| 515 |
+
|
| 516 |
+
def run(
|
| 517 |
+
self,
|
| 518 |
+
user_input: str,
|
| 519 |
+
jurisdictions: List[str],
|
| 520 |
+
activities: List[str],
|
| 521 |
+
token_description: Optional[str] = None
|
| 522 |
+
) -> ComplianceState:
|
| 523 |
+
"""
|
| 524 |
+
Run the compliance analysis workflow.
|
| 525 |
+
|
| 526 |
+
Args:
|
| 527 |
+
user_input: Business description
|
| 528 |
+
jurisdictions: List of jurisdictions to analyze
|
| 529 |
+
activities: List of crypto activities
|
| 530 |
+
token_description: Optional token description for classification
|
| 531 |
+
|
| 532 |
+
Returns:
|
| 533 |
+
Final state with complete analysis
|
| 534 |
+
"""
|
| 535 |
+
logger.info(f"Starting compliance analysis for {len(jurisdictions)} jurisdictions, {len(activities)} activities")
|
| 536 |
+
|
| 537 |
+
# Initialize state
|
| 538 |
+
initial_state: ComplianceState = {
|
| 539 |
+
'user_input': user_input,
|
| 540 |
+
'jurisdictions': jurisdictions,
|
| 541 |
+
'activities': activities,
|
| 542 |
+
'token_description': token_description,
|
| 543 |
+
'business_type': None,
|
| 544 |
+
'extracted_entities': None,
|
| 545 |
+
'token_classification': None,
|
| 546 |
+
'relevant_regulations': None,
|
| 547 |
+
'compliance_gaps': None,
|
| 548 |
+
'risk_score': None,
|
| 549 |
+
'cost_estimate': None,
|
| 550 |
+
'recommendations': None,
|
| 551 |
+
'reasoning': [],
|
| 552 |
+
'timestamp': datetime.now().isoformat(),
|
| 553 |
+
'errors': []
|
| 554 |
+
}
|
| 555 |
+
|
| 556 |
+
# Run workflow
|
| 557 |
+
try:
|
| 558 |
+
final_state = self.app.invoke(initial_state)
|
| 559 |
+
logger.info("Workflow completed successfully")
|
| 560 |
+
return final_state
|
| 561 |
+
|
| 562 |
+
except Exception as e:
|
| 563 |
+
logger.error(f"Workflow execution error: {e}")
|
| 564 |
+
initial_state['errors'].append(f"Workflow error: {str(e)}")
|
| 565 |
+
return initial_state
|
| 566 |
+
|
| 567 |
+
|
| 568 |
+
# Convenience function
|
| 569 |
+
def analyze_compliance(
|
| 570 |
+
user_input: str,
|
| 571 |
+
jurisdictions: List[str],
|
| 572 |
+
activities: List[str],
|
| 573 |
+
token_description: Optional[str] = None
|
| 574 |
+
) -> Dict:
|
| 575 |
+
"""
|
| 576 |
+
Quick compliance analysis.
|
| 577 |
+
|
| 578 |
+
Args:
|
| 579 |
+
user_input: Business description
|
| 580 |
+
jurisdictions: List of jurisdictions
|
| 581 |
+
activities: List of activities
|
| 582 |
+
token_description: Optional token description
|
| 583 |
+
|
| 584 |
+
Returns:
|
| 585 |
+
Analysis results dictionary
|
| 586 |
+
"""
|
| 587 |
+
agent = ComplianceAgent()
|
| 588 |
+
result = agent.run(user_input, jurisdictions, activities, token_description)
|
| 589 |
+
return dict(result)
|
| 590 |
+
|
| 591 |
+
|
| 592 |
+
if __name__ == "__main__":
|
| 593 |
+
# Test the agent
|
| 594 |
+
print("\n=== Testing Compliance Agent ===\n")
|
| 595 |
+
|
| 596 |
+
test_input = """
|
| 597 |
+
We are launching a crypto exchange platform that allows users to trade
|
| 598 |
+
Bitcoin, Ethereum, and other cryptocurrencies. We will provide custody
|
| 599 |
+
services and allow users to stake their tokens to earn yields. The platform
|
| 600 |
+
will operate in the US and EU.
|
| 601 |
+
"""
|
| 602 |
+
|
| 603 |
+
result = analyze_compliance(
|
| 604 |
+
user_input=test_input,
|
| 605 |
+
jurisdictions=['us', 'eu'],
|
| 606 |
+
activities=['exchange', 'custody', 'staking'],
|
| 607 |
+
token_description=None
|
| 608 |
+
)
|
| 609 |
+
|
| 610 |
+
print(f"Business Type: {result['business_type']}")
|
| 611 |
+
print(f"Risk Score: {result['risk_score']}/100")
|
| 612 |
+
print(f"\nCompliance Gaps: {len(result.get('compliance_gaps', []))}")
|
| 613 |
+
print(f"\nRecommendations:")
|
| 614 |
+
for rec in result.get('recommendations', [])[:5]:
|
| 615 |
+
print(f" - {rec}")
|
| 616 |
+
|
| 617 |
+
print(f"\nReasoning Chain:")
|
| 618 |
+
for step in result.get('reasoning', []):
|
| 619 |
+
print(f" → {step}")
|
| 620 |
+
|
| 621 |
+
print("\n=== Agent test complete! ===\n")
|
src/agents/tools.py
ADDED
|
@@ -0,0 +1,419 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Agent tools for LangChain integration.
|
| 3 |
+
Tools that the compliance agent can use to perform tasks.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import logging
|
| 7 |
+
from typing import Dict, List, Optional
|
| 8 |
+
from datetime import datetime, timedelta
|
| 9 |
+
from langchain.tools import tool
|
| 10 |
+
|
| 11 |
+
logger = logging.getLogger(__name__)
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
@tool
|
| 15 |
+
def search_regulations(
|
| 16 |
+
query: str,
|
| 17 |
+
jurisdiction: Optional[str] = None,
|
| 18 |
+
top_k: int = 5
|
| 19 |
+
) -> List[Dict]:
|
| 20 |
+
"""
|
| 21 |
+
Search for relevant regulations in the vector database.
|
| 22 |
+
|
| 23 |
+
Args:
|
| 24 |
+
query: Search query describing compliance requirements
|
| 25 |
+
jurisdiction: Filter by jurisdiction (us/eu/singapore/uk/uae) or None for all
|
| 26 |
+
top_k: Number of results to return
|
| 27 |
+
|
| 28 |
+
Returns:
|
| 29 |
+
List of relevant regulations with metadata
|
| 30 |
+
"""
|
| 31 |
+
try:
|
| 32 |
+
from src.data.vectordb import RegulatoryVectorDB
|
| 33 |
+
|
| 34 |
+
db = RegulatoryVectorDB()
|
| 35 |
+
results = db.search_relevant_regulations(
|
| 36 |
+
query=query,
|
| 37 |
+
jurisdiction=jurisdiction,
|
| 38 |
+
top_k=top_k
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
logger.info(
|
| 42 |
+
f"Found {len(results)} regulations for query: '{query}' "
|
| 43 |
+
f"(jurisdiction: {jurisdiction or 'all'})"
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
return results
|
| 47 |
+
|
| 48 |
+
except Exception as e:
|
| 49 |
+
logger.error(f"Error searching regulations: {e}")
|
| 50 |
+
return []
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
@tool
|
| 54 |
+
def calculate_compliance_cost(
|
| 55 |
+
jurisdictions: List[str],
|
| 56 |
+
activities: List[str],
|
| 57 |
+
is_security_token: bool = False
|
| 58 |
+
) -> Dict:
|
| 59 |
+
"""
|
| 60 |
+
Calculate estimated compliance costs for given jurisdictions and activities.
|
| 61 |
+
|
| 62 |
+
Args:
|
| 63 |
+
jurisdictions: List of jurisdictions (e.g., ['us', 'eu'])
|
| 64 |
+
activities: List of crypto activities (e.g., ['exchange', 'custody'])
|
| 65 |
+
is_security_token: Whether token is classified as a security
|
| 66 |
+
|
| 67 |
+
Returns:
|
| 68 |
+
Dictionary with cost estimates per jurisdiction
|
| 69 |
+
"""
|
| 70 |
+
# Cost database (approximate ranges in USD)
|
| 71 |
+
COST_DATABASE = {
|
| 72 |
+
'us': {
|
| 73 |
+
'exchange': {'first_year': (200000, 500000), 'annual': (100000, 250000)},
|
| 74 |
+
'custody': {'first_year': (100000, 300000), 'annual': (50000, 150000)},
|
| 75 |
+
'staking': {'first_year': (50000, 150000), 'annual': (25000, 75000)},
|
| 76 |
+
'lending': {'first_year': (100000, 250000), 'annual': (50000, 125000)},
|
| 77 |
+
'token_issuance': {'first_year': (50000, 200000), 'annual': (25000, 100000)},
|
| 78 |
+
'security_token_premium': {'first_year': (200000, 500000), 'annual': (100000, 250000)},
|
| 79 |
+
'base': {'first_year': (50000, 100000), 'annual': (25000, 50000)}
|
| 80 |
+
},
|
| 81 |
+
'eu': {
|
| 82 |
+
'exchange': {'first_year': (150000, 400000), 'annual': (75000, 200000)},
|
| 83 |
+
'custody': {'first_year': (100000, 250000), 'annual': (50000, 125000)},
|
| 84 |
+
'staking': {'first_year': (50000, 125000), 'annual': (25000, 60000)},
|
| 85 |
+
'lending': {'first_year': (75000, 200000), 'annual': (40000, 100000)},
|
| 86 |
+
'token_issuance': {'first_year': (100000, 300000), 'annual': (50000, 150000)},
|
| 87 |
+
'base': {'first_year': (50000, 100000), 'annual': (25000, 50000)}
|
| 88 |
+
},
|
| 89 |
+
'singapore': {
|
| 90 |
+
'exchange': {'first_year': (150000, 350000), 'annual': (75000, 175000)},
|
| 91 |
+
'custody': {'first_year': (75000, 200000), 'annual': (40000, 100000)},
|
| 92 |
+
'staking': {'first_year': (40000, 100000), 'annual': (20000, 50000)},
|
| 93 |
+
'lending': {'first_year': (60000, 150000), 'annual': (30000, 75000)},
|
| 94 |
+
'token_issuance': {'first_year': (75000, 250000), 'annual': (40000, 125000)},
|
| 95 |
+
'base': {'first_year': (40000, 80000), 'annual': (20000, 40000)}
|
| 96 |
+
},
|
| 97 |
+
'uk': {
|
| 98 |
+
'exchange': {'first_year': (125000, 300000), 'annual': (60000, 150000)},
|
| 99 |
+
'custody': {'first_year': (75000, 200000), 'annual': (40000, 100000)},
|
| 100 |
+
'staking': {'first_year': (40000, 100000), 'annual': (20000, 50000)},
|
| 101 |
+
'lending': {'first_year': (60000, 150000), 'annual': (30000, 75000)},
|
| 102 |
+
'token_issuance': {'first_year': (50000, 150000), 'annual': (25000, 75000)},
|
| 103 |
+
'base': {'first_year': (40000, 75000), 'annual': (20000, 40000)}
|
| 104 |
+
},
|
| 105 |
+
'uae': {
|
| 106 |
+
'exchange': {'first_year': (100000, 250000), 'annual': (50000, 125000)},
|
| 107 |
+
'custody': {'first_year': (60000, 150000), 'annual': (30000, 75000)},
|
| 108 |
+
'staking': {'first_year': (30000, 80000), 'annual': (15000, 40000)},
|
| 109 |
+
'lending': {'first_year': (50000, 125000), 'annual': (25000, 60000)},
|
| 110 |
+
'token_issuance': {'first_year': (50000, 150000), 'annual': (25000, 75000)},
|
| 111 |
+
'base': {'first_year': (30000, 60000), 'annual': (15000, 30000)}
|
| 112 |
+
}
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
results = {}
|
| 116 |
+
|
| 117 |
+
for jurisdiction in jurisdictions:
|
| 118 |
+
if jurisdiction not in COST_DATABASE:
|
| 119 |
+
logger.warning(f"Unknown jurisdiction: {jurisdiction}")
|
| 120 |
+
continue
|
| 121 |
+
|
| 122 |
+
costs = COST_DATABASE[jurisdiction]
|
| 123 |
+
first_year_min = 0
|
| 124 |
+
first_year_max = 0
|
| 125 |
+
annual_min = 0
|
| 126 |
+
annual_max = 0
|
| 127 |
+
|
| 128 |
+
# Base costs
|
| 129 |
+
first_year_min += costs['base']['first_year'][0]
|
| 130 |
+
first_year_max += costs['base']['first_year'][1]
|
| 131 |
+
annual_min += costs['base']['annual'][0]
|
| 132 |
+
annual_max += costs['base']['annual'][1]
|
| 133 |
+
|
| 134 |
+
# Activity-specific costs
|
| 135 |
+
for activity in activities:
|
| 136 |
+
if activity in costs:
|
| 137 |
+
first_year_min += costs[activity]['first_year'][0]
|
| 138 |
+
first_year_max += costs[activity]['first_year'][1]
|
| 139 |
+
annual_min += costs[activity]['annual'][0]
|
| 140 |
+
annual_max += costs[activity]['annual'][1]
|
| 141 |
+
|
| 142 |
+
# Security token premium (US)
|
| 143 |
+
if is_security_token and jurisdiction == 'us':
|
| 144 |
+
first_year_min += costs['security_token_premium']['first_year'][0]
|
| 145 |
+
first_year_max += costs['security_token_premium']['first_year'][1]
|
| 146 |
+
annual_min += costs['security_token_premium']['annual'][0]
|
| 147 |
+
annual_max += costs['security_token_premium']['annual'][1]
|
| 148 |
+
|
| 149 |
+
results[jurisdiction] = {
|
| 150 |
+
'first_year': {
|
| 151 |
+
'min': first_year_min,
|
| 152 |
+
'max': first_year_max,
|
| 153 |
+
'estimate': (first_year_min + first_year_max) // 2
|
| 154 |
+
},
|
| 155 |
+
'annual_ongoing': {
|
| 156 |
+
'min': annual_min,
|
| 157 |
+
'max': annual_max,
|
| 158 |
+
'estimate': (annual_min + annual_max) // 2
|
| 159 |
+
},
|
| 160 |
+
'breakdown': {
|
| 161 |
+
'base_compliance': costs['base'],
|
| 162 |
+
'activities': [
|
| 163 |
+
{'activity': act, 'cost': costs.get(act, {'first_year': (0, 0), 'annual': (0, 0)})}
|
| 164 |
+
for act in activities if act in costs
|
| 165 |
+
]
|
| 166 |
+
}
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
logger.info(
|
| 170 |
+
f"Calculated costs for {len(results)} jurisdictions, "
|
| 171 |
+
f"{len(activities)} activities, security_token={is_security_token}"
|
| 172 |
+
)
|
| 173 |
+
|
| 174 |
+
return results
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
@tool
|
| 178 |
+
def check_effective_date(regulation_id: str) -> Dict:
|
| 179 |
+
"""
|
| 180 |
+
Check if a regulation is currently effective or pending.
|
| 181 |
+
|
| 182 |
+
Args:
|
| 183 |
+
regulation_id: ID of the regulation to check
|
| 184 |
+
|
| 185 |
+
Returns:
|
| 186 |
+
Dictionary with status information
|
| 187 |
+
"""
|
| 188 |
+
try:
|
| 189 |
+
from src.data.vectordb import RegulatoryVectorDB
|
| 190 |
+
|
| 191 |
+
db = RegulatoryVectorDB()
|
| 192 |
+
regulation = db.get_regulation_by_id(regulation_id)
|
| 193 |
+
|
| 194 |
+
if not regulation:
|
| 195 |
+
return {
|
| 196 |
+
'found': False,
|
| 197 |
+
'message': f"Regulation {regulation_id} not found"
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
status = regulation.get('status', 'unknown')
|
| 201 |
+
effective_date_str = regulation.get('effective_date', '')
|
| 202 |
+
announced_date_str = regulation.get('announced_date', '')
|
| 203 |
+
|
| 204 |
+
result = {
|
| 205 |
+
'found': True,
|
| 206 |
+
'regulation_id': regulation_id,
|
| 207 |
+
'title': regulation.get('title', ''),
|
| 208 |
+
'status': status,
|
| 209 |
+
'announced_date': announced_date_str,
|
| 210 |
+
'effective_date': effective_date_str,
|
| 211 |
+
'is_effective': status == 'effective',
|
| 212 |
+
'is_proposed': status == 'proposed',
|
| 213 |
+
'is_repealed': status == 'repealed'
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
# Calculate time until effective (if applicable)
|
| 217 |
+
if effective_date_str and status == 'proposed':
|
| 218 |
+
try:
|
| 219 |
+
effective_date = datetime.fromisoformat(effective_date_str.replace('Z', '+00:00'))
|
| 220 |
+
now = datetime.now(effective_date.tzinfo) if effective_date.tzinfo else datetime.now()
|
| 221 |
+
days_until = (effective_date - now).days
|
| 222 |
+
|
| 223 |
+
result['days_until_effective'] = days_until
|
| 224 |
+
result['time_to_comply'] = f"{days_until} days" if days_until > 0 else "Overdue"
|
| 225 |
+
|
| 226 |
+
except Exception as e:
|
| 227 |
+
logger.warning(f"Could not parse effective date: {e}")
|
| 228 |
+
|
| 229 |
+
logger.info(f"Checked regulation {regulation_id}: status={status}")
|
| 230 |
+
|
| 231 |
+
return result
|
| 232 |
+
|
| 233 |
+
except Exception as e:
|
| 234 |
+
logger.error(f"Error checking effective date: {e}")
|
| 235 |
+
return {
|
| 236 |
+
'found': False,
|
| 237 |
+
'error': str(e)
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
@tool
|
| 242 |
+
def analyze_token_security(token_description: str, jurisdiction: str = 'us') -> Dict:
|
| 243 |
+
"""
|
| 244 |
+
Analyze whether a token is a security using the Howey Test or other frameworks.
|
| 245 |
+
|
| 246 |
+
Args:
|
| 247 |
+
token_description: Description of the token's functionality and economics
|
| 248 |
+
jurisdiction: Jurisdiction for classification (us/eu/singapore)
|
| 249 |
+
|
| 250 |
+
Returns:
|
| 251 |
+
Token classification result with confidence score
|
| 252 |
+
"""
|
| 253 |
+
try:
|
| 254 |
+
from src.analysis.token_classifier import classify_token
|
| 255 |
+
|
| 256 |
+
result = classify_token(token_description, jurisdiction=jurisdiction)
|
| 257 |
+
|
| 258 |
+
# Format for agent consumption
|
| 259 |
+
formatted = {
|
| 260 |
+
'jurisdiction': jurisdiction,
|
| 261 |
+
'classification': result.get('classification', 'unknown'),
|
| 262 |
+
'confidence': result.get('confidence', 0.0),
|
| 263 |
+
'framework': result.get('framework', ''),
|
| 264 |
+
'implications': result.get('regulatory_implications', []),
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
# Add Howey Test details for US
|
| 268 |
+
if jurisdiction == 'us' and 'howey_test' in result:
|
| 269 |
+
howey = result['howey_test']
|
| 270 |
+
formatted['howey_test'] = {
|
| 271 |
+
'prongs_met': howey.get('prongs_met', 0),
|
| 272 |
+
'is_security': howey.get('is_security', False),
|
| 273 |
+
'prongs': {
|
| 274 |
+
name: data.get('met', False)
|
| 275 |
+
for name, data in howey.get('prongs', {}).items()
|
| 276 |
+
}
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
logger.info(
|
| 280 |
+
f"Token analysis ({jurisdiction}): {formatted['classification']} "
|
| 281 |
+
f"(confidence: {formatted['confidence']:.2f})"
|
| 282 |
+
)
|
| 283 |
+
|
| 284 |
+
return formatted
|
| 285 |
+
|
| 286 |
+
except Exception as e:
|
| 287 |
+
logger.error(f"Error analyzing token: {e}")
|
| 288 |
+
return {
|
| 289 |
+
'error': str(e),
|
| 290 |
+
'classification': 'error'
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
|
| 294 |
+
@tool
|
| 295 |
+
def extract_entities_from_text(text: str) -> Dict:
|
| 296 |
+
"""
|
| 297 |
+
Extract financial and crypto entities from text.
|
| 298 |
+
|
| 299 |
+
Args:
|
| 300 |
+
text: Input text to analyze
|
| 301 |
+
|
| 302 |
+
Returns:
|
| 303 |
+
Dictionary of extracted entities
|
| 304 |
+
"""
|
| 305 |
+
try:
|
| 306 |
+
from src.processors.entity_extraction import extract_entities
|
| 307 |
+
|
| 308 |
+
entities = extract_entities(text)
|
| 309 |
+
|
| 310 |
+
# Simplify for agent consumption
|
| 311 |
+
simplified = {
|
| 312 |
+
'summary': entities.get('summary', {}),
|
| 313 |
+
'amounts': [e['text'] for e in entities.get('financial', {}).get('amounts', [])],
|
| 314 |
+
'dates': [e['text'] for e in entities.get('financial', {}).get('dates', [])],
|
| 315 |
+
'institutions': [e['text'] for e in entities.get('financial', {}).get('institutions', [])],
|
| 316 |
+
'tokens': [e['name'] for e in entities.get('crypto', {}).get('tokens', [])],
|
| 317 |
+
'protocols': [e['name'] for e in entities.get('crypto', {}).get('protocols', [])],
|
| 318 |
+
'activities': list(set([e['activity'] for e in entities.get('crypto', {}).get('activities', [])]))
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
logger.info(f"Extracted {entities['summary']['total_entities']} entities from text")
|
| 322 |
+
|
| 323 |
+
return simplified
|
| 324 |
+
|
| 325 |
+
except Exception as e:
|
| 326 |
+
logger.error(f"Error extracting entities: {e}")
|
| 327 |
+
return {'error': str(e)}
|
| 328 |
+
|
| 329 |
+
|
| 330 |
+
@tool
|
| 331 |
+
def parse_document(file_path: str) -> Dict:
|
| 332 |
+
"""
|
| 333 |
+
Parse a document and extract text and metadata.
|
| 334 |
+
|
| 335 |
+
Args:
|
| 336 |
+
file_path: Path to document file
|
| 337 |
+
|
| 338 |
+
Returns:
|
| 339 |
+
Parsed document with text, type, and metadata
|
| 340 |
+
"""
|
| 341 |
+
try:
|
| 342 |
+
from src.processors.document_parser import parse_document as parse_doc
|
| 343 |
+
|
| 344 |
+
result = parse_doc(file_path)
|
| 345 |
+
|
| 346 |
+
# Simplify for agent
|
| 347 |
+
simplified = {
|
| 348 |
+
'text': result.get('text', ''),
|
| 349 |
+
'document_type': result.get('document_type', 'unknown'),
|
| 350 |
+
'confidence': result.get('type_confidence', 0.0),
|
| 351 |
+
'word_count': result.get('word_count', 0),
|
| 352 |
+
'char_count': result.get('char_count', 0),
|
| 353 |
+
'filename': result.get('metadata', {}).get('filename', '')
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
logger.info(
|
| 357 |
+
f"Parsed document: {simplified['filename']} "
|
| 358 |
+
f"(type: {simplified['document_type']}, {simplified['word_count']} words)"
|
| 359 |
+
)
|
| 360 |
+
|
| 361 |
+
return simplified
|
| 362 |
+
|
| 363 |
+
except Exception as e:
|
| 364 |
+
logger.error(f"Error parsing document: {e}")
|
| 365 |
+
return {'error': str(e)}
|
| 366 |
+
|
| 367 |
+
|
| 368 |
+
# Tool list for LangChain agent
|
| 369 |
+
COMPLIANCE_TOOLS = [
|
| 370 |
+
search_regulations,
|
| 371 |
+
calculate_compliance_cost,
|
| 372 |
+
check_effective_date,
|
| 373 |
+
analyze_token_security,
|
| 374 |
+
extract_entities_from_text,
|
| 375 |
+
parse_document
|
| 376 |
+
]
|
| 377 |
+
|
| 378 |
+
|
| 379 |
+
if __name__ == "__main__":
|
| 380 |
+
# Test tools
|
| 381 |
+
print("\n=== Testing Agent Tools ===\n")
|
| 382 |
+
|
| 383 |
+
# Test 1: Search regulations
|
| 384 |
+
print("1. Testing search_regulations...")
|
| 385 |
+
results = search_regulations.invoke({
|
| 386 |
+
"query": "crypto custody requirements",
|
| 387 |
+
"jurisdiction": "us",
|
| 388 |
+
"top_k": 3
|
| 389 |
+
})
|
| 390 |
+
print(f" Found {len(results)} regulations")
|
| 391 |
+
|
| 392 |
+
# Test 2: Calculate costs
|
| 393 |
+
print("\n2. Testing calculate_compliance_cost...")
|
| 394 |
+
costs = calculate_compliance_cost.invoke({
|
| 395 |
+
"jurisdictions": ["us", "eu"],
|
| 396 |
+
"activities": ["exchange", "custody"],
|
| 397 |
+
"is_security_token": False
|
| 398 |
+
})
|
| 399 |
+
for jur, cost_data in costs.items():
|
| 400 |
+
print(f" {jur.upper()}: ${cost_data['first_year']['estimate']:,} first year")
|
| 401 |
+
|
| 402 |
+
# Test 3: Token analysis
|
| 403 |
+
print("\n3. Testing analyze_token_security...")
|
| 404 |
+
token_result = analyze_token_security.invoke({
|
| 405 |
+
"token_description": "Investors buy tokens to earn profits from platform growth",
|
| 406 |
+
"jurisdiction": "us"
|
| 407 |
+
})
|
| 408 |
+
print(f" Classification: {token_result['classification']}")
|
| 409 |
+
print(f" Confidence: {token_result['confidence']:.2f}")
|
| 410 |
+
|
| 411 |
+
# Test 4: Entity extraction
|
| 412 |
+
print("\n4. Testing extract_entities_from_text...")
|
| 413 |
+
text = "SEC announced $10 million fine for Coinbase on January 15, 2024"
|
| 414 |
+
entities = extract_entities_from_text.invoke({"text": text})
|
| 415 |
+
print(f" Amounts: {entities['amounts']}")
|
| 416 |
+
print(f" Dates: {entities['dates']}")
|
| 417 |
+
print(f" Institutions: {entities['institutions']}")
|
| 418 |
+
|
| 419 |
+
print("\n=== All tools tested successfully! ===\n")
|
src/analysis/__init__.py
ADDED
|
File without changes
|
src/analysis/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (200 Bytes). View file
|
|
|
src/analysis/__pycache__/compliance_engine.cpython-313.pyc
ADDED
|
Binary file (15.7 kB). View file
|
|
|
src/analysis/__pycache__/cost_calculator.cpython-313.pyc
ADDED
|
Binary file (11.8 kB). View file
|
|
|
src/analysis/__pycache__/risk_scorer.cpython-313.pyc
ADDED
|
Binary file (17.2 kB). View file
|
|
|
src/analysis/__pycache__/token_classifier.cpython-313.pyc
ADDED
|
Binary file (15.7 kB). View file
|
|
|
src/analysis/compliance_engine.py
ADDED
|
@@ -0,0 +1,486 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Compliance Engine for structured rule matching and gap identification.
|
| 3 |
+
Provides deterministic compliance analysis without relying solely on LLM prompts.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import logging
|
| 7 |
+
from typing import Dict, List, Optional, Set
|
| 8 |
+
from datetime import datetime, timedelta
|
| 9 |
+
from enum import Enum
|
| 10 |
+
|
| 11 |
+
logger = logging.getLogger(__name__)
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class Severity(Enum):
|
| 15 |
+
"""Severity levels for compliance gaps."""
|
| 16 |
+
CRITICAL = "critical" # Immediate action required, high enforcement risk
|
| 17 |
+
HIGH = "high" # Action needed within 3 months
|
| 18 |
+
MEDIUM = "medium" # Action needed within 6 months
|
| 19 |
+
LOW = "low" # Monitor, action within 1 year
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class Urgency(Enum):
|
| 23 |
+
"""Urgency levels based on deadlines."""
|
| 24 |
+
IMMEDIATE = "immediate" # < 30 days
|
| 25 |
+
URGENT = "urgent" # 30-90 days
|
| 26 |
+
MODERATE = "moderate" # 90-180 days
|
| 27 |
+
PLANNING = "planning" # > 180 days
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class ComplianceGap:
|
| 31 |
+
"""Represents a single compliance gap."""
|
| 32 |
+
|
| 33 |
+
def __init__(
|
| 34 |
+
self,
|
| 35 |
+
requirement: str,
|
| 36 |
+
jurisdiction: str,
|
| 37 |
+
activity: str,
|
| 38 |
+
severity: Severity,
|
| 39 |
+
urgency: Urgency,
|
| 40 |
+
regulation_id: Optional[str] = None,
|
| 41 |
+
deadline: Optional[str] = None,
|
| 42 |
+
cost_estimate: Optional[Dict] = None,
|
| 43 |
+
remediation_steps: Optional[List[str]] = None
|
| 44 |
+
):
|
| 45 |
+
self.requirement = requirement
|
| 46 |
+
self.jurisdiction = jurisdiction
|
| 47 |
+
self.activity = activity
|
| 48 |
+
self.severity = severity
|
| 49 |
+
self.urgency = urgency
|
| 50 |
+
self.regulation_id = regulation_id
|
| 51 |
+
self.deadline = deadline
|
| 52 |
+
self.cost_estimate = cost_estimate
|
| 53 |
+
self.remediation_steps = remediation_steps or []
|
| 54 |
+
|
| 55 |
+
def to_dict(self) -> Dict:
|
| 56 |
+
"""Convert to dictionary."""
|
| 57 |
+
return {
|
| 58 |
+
'requirement': self.requirement,
|
| 59 |
+
'jurisdiction': self.jurisdiction,
|
| 60 |
+
'activity': self.activity,
|
| 61 |
+
'severity': self.severity.value,
|
| 62 |
+
'urgency': self.urgency.value,
|
| 63 |
+
'regulation_id': self.regulation_id,
|
| 64 |
+
'deadline': self.deadline,
|
| 65 |
+
'cost_estimate': self.cost_estimate,
|
| 66 |
+
'remediation_steps': self.remediation_steps
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
class ComplianceEngine:
|
| 71 |
+
"""
|
| 72 |
+
Structured compliance engine for rule matching and gap analysis.
|
| 73 |
+
|
| 74 |
+
Maps activities to requirements per jurisdiction and identifies gaps.
|
| 75 |
+
"""
|
| 76 |
+
|
| 77 |
+
def __init__(self):
|
| 78 |
+
"""Initialize compliance engine with requirement mappings."""
|
| 79 |
+
self.requirements = self._build_requirements_database()
|
| 80 |
+
logger.info("ComplianceEngine initialized with requirements database")
|
| 81 |
+
|
| 82 |
+
def _build_requirements_database(self) -> Dict:
|
| 83 |
+
"""
|
| 84 |
+
Build requirements database mapping activities to compliance requirements.
|
| 85 |
+
|
| 86 |
+
Structure: {jurisdiction: {activity: [requirements]}}
|
| 87 |
+
"""
|
| 88 |
+
return {
|
| 89 |
+
'us': {
|
| 90 |
+
'exchange': [
|
| 91 |
+
{
|
| 92 |
+
'requirement': 'FinCEN MSB Registration',
|
| 93 |
+
'severity': Severity.CRITICAL,
|
| 94 |
+
'deadline_days': 180,
|
| 95 |
+
'description': 'Register as Money Services Business with FinCEN',
|
| 96 |
+
'cost_range': (10000, 25000),
|
| 97 |
+
'steps': [
|
| 98 |
+
'File FinCEN Form 107',
|
| 99 |
+
'Implement AML/KYC program',
|
| 100 |
+
'Appoint compliance officer'
|
| 101 |
+
]
|
| 102 |
+
},
|
| 103 |
+
{
|
| 104 |
+
'requirement': 'State Money Transmitter Licenses',
|
| 105 |
+
'severity': Severity.CRITICAL,
|
| 106 |
+
'deadline_days': 365,
|
| 107 |
+
'description': 'Obtain MTL in each state of operation',
|
| 108 |
+
'cost_range': (50000, 150000),
|
| 109 |
+
'steps': [
|
| 110 |
+
'File applications per state',
|
| 111 |
+
'Post surety bonds',
|
| 112 |
+
'Meet minimum capital requirements'
|
| 113 |
+
]
|
| 114 |
+
}
|
| 115 |
+
],
|
| 116 |
+
'custody': [
|
| 117 |
+
{
|
| 118 |
+
'requirement': 'SEC Custody Rule Compliance',
|
| 119 |
+
'severity': Severity.HIGH,
|
| 120 |
+
'deadline_days': 180,
|
| 121 |
+
'description': 'Comply with SEC custody and safeguarding rules',
|
| 122 |
+
'cost_range': (75000, 200000),
|
| 123 |
+
'steps': [
|
| 124 |
+
'Implement qualified custody solution',
|
| 125 |
+
'Annual surprise audits',
|
| 126 |
+
'Insurance requirements'
|
| 127 |
+
]
|
| 128 |
+
}
|
| 129 |
+
],
|
| 130 |
+
'staking': [
|
| 131 |
+
{
|
| 132 |
+
'requirement': 'Securities Law Review',
|
| 133 |
+
'severity': Severity.HIGH,
|
| 134 |
+
'deadline_days': 90,
|
| 135 |
+
'description': 'Determine if staking constitutes securities offering',
|
| 136 |
+
'cost_range': (25000, 75000),
|
| 137 |
+
'steps': [
|
| 138 |
+
'Legal counsel review',
|
| 139 |
+
'Howey Test analysis',
|
| 140 |
+
'Consider SEC exemptions'
|
| 141 |
+
]
|
| 142 |
+
}
|
| 143 |
+
],
|
| 144 |
+
'token_issuance': [
|
| 145 |
+
{
|
| 146 |
+
'requirement': 'SEC Registration or Exemption',
|
| 147 |
+
'severity': Severity.CRITICAL,
|
| 148 |
+
'deadline_days': 180,
|
| 149 |
+
'description': 'Register securities or qualify for exemption',
|
| 150 |
+
'cost_range': (100000, 500000),
|
| 151 |
+
'steps': [
|
| 152 |
+
'Determine if token is security',
|
| 153 |
+
'File Form D (Reg D) or Form 1-A (Reg A+)',
|
| 154 |
+
'Accredited investor verification'
|
| 155 |
+
]
|
| 156 |
+
}
|
| 157 |
+
]
|
| 158 |
+
},
|
| 159 |
+
'eu': {
|
| 160 |
+
'exchange': [
|
| 161 |
+
{
|
| 162 |
+
'requirement': 'MiCA CASP Authorization',
|
| 163 |
+
'severity': Severity.CRITICAL,
|
| 164 |
+
'deadline_days': 365,
|
| 165 |
+
'description': 'Obtain Crypto-Asset Service Provider authorization',
|
| 166 |
+
'cost_range': (100000, 300000),
|
| 167 |
+
'steps': [
|
| 168 |
+
'Submit application to national regulator',
|
| 169 |
+
'Meet capital requirements',
|
| 170 |
+
'Implement governance framework'
|
| 171 |
+
]
|
| 172 |
+
},
|
| 173 |
+
{
|
| 174 |
+
'requirement': 'AMLD5 Compliance',
|
| 175 |
+
'severity': Severity.CRITICAL,
|
| 176 |
+
'deadline_days': 180,
|
| 177 |
+
'description': 'Anti-Money Laundering Directive compliance',
|
| 178 |
+
'cost_range': (50000, 150000),
|
| 179 |
+
'steps': [
|
| 180 |
+
'Customer due diligence procedures',
|
| 181 |
+
'Transaction monitoring',
|
| 182 |
+
'Suspicious activity reporting'
|
| 183 |
+
]
|
| 184 |
+
}
|
| 185 |
+
],
|
| 186 |
+
'custody': [
|
| 187 |
+
{
|
| 188 |
+
'requirement': 'MiCA Custody Requirements',
|
| 189 |
+
'severity': Severity.HIGH,
|
| 190 |
+
'deadline_days': 180,
|
| 191 |
+
'description': 'Safeguarding of client crypto-assets',
|
| 192 |
+
'cost_range': (75000, 200000),
|
| 193 |
+
'steps': [
|
| 194 |
+
'Segregation of client assets',
|
| 195 |
+
'Professional indemnity insurance',
|
| 196 |
+
'Custodian arrangements'
|
| 197 |
+
]
|
| 198 |
+
}
|
| 199 |
+
],
|
| 200 |
+
'token_issuance': [
|
| 201 |
+
{
|
| 202 |
+
'requirement': 'MiCA White Paper',
|
| 203 |
+
'severity': Severity.HIGH,
|
| 204 |
+
'deadline_days': 90,
|
| 205 |
+
'description': 'Publish and notify white paper to regulator',
|
| 206 |
+
'cost_range': (25000, 75000),
|
| 207 |
+
'steps': [
|
| 208 |
+
'Draft comprehensive white paper',
|
| 209 |
+
'Notify competent authority',
|
| 210 |
+
'Ongoing disclosure obligations'
|
| 211 |
+
]
|
| 212 |
+
}
|
| 213 |
+
]
|
| 214 |
+
},
|
| 215 |
+
'singapore': {
|
| 216 |
+
'exchange': [
|
| 217 |
+
{
|
| 218 |
+
'requirement': 'MAS DPT License',
|
| 219 |
+
'severity': Severity.CRITICAL,
|
| 220 |
+
'deadline_days': 365,
|
| 221 |
+
'description': 'Digital Payment Token service license',
|
| 222 |
+
'cost_range': (75000, 250000),
|
| 223 |
+
'steps': [
|
| 224 |
+
'Submit MAS license application',
|
| 225 |
+
'Meet fit and proper criteria',
|
| 226 |
+
'Implement technology risk management'
|
| 227 |
+
]
|
| 228 |
+
}
|
| 229 |
+
],
|
| 230 |
+
'custody': [
|
| 231 |
+
{
|
| 232 |
+
'requirement': 'DPT Custody Standards',
|
| 233 |
+
'severity': Severity.HIGH,
|
| 234 |
+
'deadline_days': 180,
|
| 235 |
+
'description': 'MAS guidelines for DPT custody',
|
| 236 |
+
'cost_range': (50000, 150000),
|
| 237 |
+
'steps': [
|
| 238 |
+
'Segregation of customer DPTs',
|
| 239 |
+
'Cold storage requirements',
|
| 240 |
+
'Insurance or capital reserves'
|
| 241 |
+
]
|
| 242 |
+
}
|
| 243 |
+
]
|
| 244 |
+
},
|
| 245 |
+
'uk': {
|
| 246 |
+
'exchange': [
|
| 247 |
+
{
|
| 248 |
+
'requirement': 'FCA Cryptoasset Registration',
|
| 249 |
+
'severity': Severity.CRITICAL,
|
| 250 |
+
'deadline_days': 365,
|
| 251 |
+
'description': 'Register with FCA for AML/CTF',
|
| 252 |
+
'cost_range': (50000, 150000),
|
| 253 |
+
'steps': [
|
| 254 |
+
'FCA registration application',
|
| 255 |
+
'AML/CTF procedures',
|
| 256 |
+
'Senior management approval'
|
| 257 |
+
]
|
| 258 |
+
},
|
| 259 |
+
{
|
| 260 |
+
'requirement': 'Financial Promotions Compliance',
|
| 261 |
+
'severity': Severity.HIGH,
|
| 262 |
+
'deadline_days': 90,
|
| 263 |
+
'description': 'Comply with cryptoasset promotions regime',
|
| 264 |
+
'cost_range': (15000, 50000),
|
| 265 |
+
'steps': [
|
| 266 |
+
'Ensure promotions are fair, clear, not misleading',
|
| 267 |
+
'Risk warnings required',
|
| 268 |
+
'Approval by authorized firm'
|
| 269 |
+
]
|
| 270 |
+
}
|
| 271 |
+
]
|
| 272 |
+
},
|
| 273 |
+
'uae': {
|
| 274 |
+
'exchange': [
|
| 275 |
+
{
|
| 276 |
+
'requirement': 'VARA VASP License',
|
| 277 |
+
'severity': Severity.CRITICAL,
|
| 278 |
+
'deadline_days': 365,
|
| 279 |
+
'description': 'Virtual Asset Service Provider license from VARA',
|
| 280 |
+
'cost_range': (75000, 200000),
|
| 281 |
+
'steps': [
|
| 282 |
+
'Submit VARA application',
|
| 283 |
+
'Meet minimum capital (AED 50k)',
|
| 284 |
+
'Comply with VARA rulebook'
|
| 285 |
+
]
|
| 286 |
+
}
|
| 287 |
+
]
|
| 288 |
+
}
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
def analyze_compliance(
|
| 292 |
+
self,
|
| 293 |
+
jurisdictions: List[str],
|
| 294 |
+
activities: List[str],
|
| 295 |
+
is_security_token: bool = False
|
| 296 |
+
) -> Dict:
|
| 297 |
+
"""
|
| 298 |
+
Analyze compliance requirements and identify gaps.
|
| 299 |
+
|
| 300 |
+
Args:
|
| 301 |
+
jurisdictions: List of jurisdictions to analyze
|
| 302 |
+
activities: List of crypto activities
|
| 303 |
+
is_security_token: Whether token is classified as security
|
| 304 |
+
|
| 305 |
+
Returns:
|
| 306 |
+
Dictionary with gaps, compliant items, and summary
|
| 307 |
+
"""
|
| 308 |
+
gaps = []
|
| 309 |
+
compliant = []
|
| 310 |
+
warnings = []
|
| 311 |
+
|
| 312 |
+
for jurisdiction in jurisdictions:
|
| 313 |
+
if jurisdiction not in self.requirements:
|
| 314 |
+
warnings.append(f"No requirements database for jurisdiction: {jurisdiction}")
|
| 315 |
+
continue
|
| 316 |
+
|
| 317 |
+
jurisdiction_reqs = self.requirements[jurisdiction]
|
| 318 |
+
|
| 319 |
+
for activity in activities:
|
| 320 |
+
if activity not in jurisdiction_reqs:
|
| 321 |
+
# No specific requirements for this activity in this jurisdiction
|
| 322 |
+
warnings.append(
|
| 323 |
+
f"No specific requirements found for {activity} in {jurisdiction}"
|
| 324 |
+
)
|
| 325 |
+
continue
|
| 326 |
+
|
| 327 |
+
requirements = jurisdiction_reqs[activity]
|
| 328 |
+
|
| 329 |
+
for req in requirements:
|
| 330 |
+
# Create compliance gap
|
| 331 |
+
urgency = self._calculate_urgency(req.get('deadline_days', 365))
|
| 332 |
+
|
| 333 |
+
gap = ComplianceGap(
|
| 334 |
+
requirement=req['requirement'],
|
| 335 |
+
jurisdiction=jurisdiction,
|
| 336 |
+
activity=activity,
|
| 337 |
+
severity=req['severity'],
|
| 338 |
+
urgency=urgency,
|
| 339 |
+
deadline=self._calculate_deadline(req.get('deadline_days', 365)),
|
| 340 |
+
cost_estimate={
|
| 341 |
+
'min': req['cost_range'][0],
|
| 342 |
+
'max': req['cost_range'][1],
|
| 343 |
+
'estimate': sum(req['cost_range']) // 2
|
| 344 |
+
},
|
| 345 |
+
remediation_steps=req.get('steps', [])
|
| 346 |
+
)
|
| 347 |
+
|
| 348 |
+
gaps.append(gap)
|
| 349 |
+
|
| 350 |
+
# Additional check for security tokens
|
| 351 |
+
if is_security_token and 'us' in jurisdictions:
|
| 352 |
+
if 'token_issuance' not in activities:
|
| 353 |
+
# Add securities registration requirement
|
| 354 |
+
sec_gap = ComplianceGap(
|
| 355 |
+
requirement='SEC Securities Registration',
|
| 356 |
+
jurisdiction='us',
|
| 357 |
+
activity='token_issuance',
|
| 358 |
+
severity=Severity.CRITICAL,
|
| 359 |
+
urgency=Urgency.IMMEDIATE,
|
| 360 |
+
deadline=self._calculate_deadline(60),
|
| 361 |
+
cost_estimate={'min': 200000, 'max': 500000, 'estimate': 350000},
|
| 362 |
+
remediation_steps=[
|
| 363 |
+
'Immediate legal counsel consultation',
|
| 364 |
+
'Howey Test confirms security status',
|
| 365 |
+
'File registration or seek exemption',
|
| 366 |
+
'Consider Regulation D or A+'
|
| 367 |
+
]
|
| 368 |
+
)
|
| 369 |
+
gaps.append(sec_gap)
|
| 370 |
+
|
| 371 |
+
# Sort gaps by severity and urgency
|
| 372 |
+
gaps.sort(key=lambda g: (
|
| 373 |
+
['critical', 'high', 'medium', 'low'].index(g.severity.value),
|
| 374 |
+
['immediate', 'urgent', 'moderate', 'planning'].index(g.urgency.value)
|
| 375 |
+
))
|
| 376 |
+
|
| 377 |
+
summary = {
|
| 378 |
+
'total_gaps': len(gaps),
|
| 379 |
+
'critical_gaps': sum(1 for g in gaps if g.severity == Severity.CRITICAL),
|
| 380 |
+
'high_gaps': sum(1 for g in gaps if g.severity == Severity.HIGH),
|
| 381 |
+
'immediate_action': sum(1 for g in gaps if g.urgency == Urgency.IMMEDIATE),
|
| 382 |
+
'estimated_total_cost': sum(
|
| 383 |
+
g.cost_estimate['estimate'] for g in gaps if g.cost_estimate
|
| 384 |
+
),
|
| 385 |
+
'jurisdictions_analyzed': len(jurisdictions),
|
| 386 |
+
'activities_analyzed': len(activities),
|
| 387 |
+
'warnings': warnings
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
logger.info(
|
| 391 |
+
f"Compliance analysis: {len(gaps)} gaps found across "
|
| 392 |
+
f"{len(jurisdictions)} jurisdictions, {len(activities)} activities"
|
| 393 |
+
)
|
| 394 |
+
|
| 395 |
+
return {
|
| 396 |
+
'gaps': [g.to_dict() for g in gaps],
|
| 397 |
+
'compliant': compliant,
|
| 398 |
+
'summary': summary,
|
| 399 |
+
'analyzed_at': datetime.now().isoformat()
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
+
def _calculate_urgency(self, deadline_days: int) -> Urgency:
|
| 403 |
+
"""Calculate urgency based on deadline."""
|
| 404 |
+
if deadline_days <= 30:
|
| 405 |
+
return Urgency.IMMEDIATE
|
| 406 |
+
elif deadline_days <= 90:
|
| 407 |
+
return Urgency.URGENT
|
| 408 |
+
elif deadline_days <= 180:
|
| 409 |
+
return Urgency.MODERATE
|
| 410 |
+
else:
|
| 411 |
+
return Urgency.PLANNING
|
| 412 |
+
|
| 413 |
+
def _calculate_deadline(self, days: int) -> str:
|
| 414 |
+
"""Calculate deadline date from days."""
|
| 415 |
+
deadline = datetime.now() + timedelta(days=days)
|
| 416 |
+
return deadline.strftime('%Y-%m-%d')
|
| 417 |
+
|
| 418 |
+
def get_requirement_details(
|
| 419 |
+
self,
|
| 420 |
+
jurisdiction: str,
|
| 421 |
+
activity: str
|
| 422 |
+
) -> Optional[List[Dict]]:
|
| 423 |
+
"""
|
| 424 |
+
Get detailed requirements for a specific jurisdiction and activity.
|
| 425 |
+
|
| 426 |
+
Args:
|
| 427 |
+
jurisdiction: Jurisdiction code
|
| 428 |
+
activity: Activity type
|
| 429 |
+
|
| 430 |
+
Returns:
|
| 431 |
+
List of requirement dictionaries or None
|
| 432 |
+
"""
|
| 433 |
+
if jurisdiction not in self.requirements:
|
| 434 |
+
return None
|
| 435 |
+
|
| 436 |
+
if activity not in self.requirements[jurisdiction]:
|
| 437 |
+
return None
|
| 438 |
+
|
| 439 |
+
return self.requirements[jurisdiction][activity]
|
| 440 |
+
|
| 441 |
+
|
| 442 |
+
# Convenience function
|
| 443 |
+
def analyze_compliance(
|
| 444 |
+
jurisdictions: List[str],
|
| 445 |
+
activities: List[str],
|
| 446 |
+
is_security_token: bool = False
|
| 447 |
+
) -> Dict:
|
| 448 |
+
"""
|
| 449 |
+
Quick compliance analysis.
|
| 450 |
+
|
| 451 |
+
Args:
|
| 452 |
+
jurisdictions: List of jurisdictions
|
| 453 |
+
activities: List of activities
|
| 454 |
+
is_security_token: Whether token is a security
|
| 455 |
+
|
| 456 |
+
Returns:
|
| 457 |
+
Analysis results
|
| 458 |
+
"""
|
| 459 |
+
engine = ComplianceEngine()
|
| 460 |
+
return engine.analyze_compliance(jurisdictions, activities, is_security_token)
|
| 461 |
+
|
| 462 |
+
|
| 463 |
+
if __name__ == "__main__":
|
| 464 |
+
# Test the engine
|
| 465 |
+
print("\n=== Testing Compliance Engine ===\n")
|
| 466 |
+
|
| 467 |
+
result = analyze_compliance(
|
| 468 |
+
jurisdictions=['us', 'eu'],
|
| 469 |
+
activities=['exchange', 'custody'],
|
| 470 |
+
is_security_token=False
|
| 471 |
+
)
|
| 472 |
+
|
| 473 |
+
print(f"Total gaps: {result['summary']['total_gaps']}")
|
| 474 |
+
print(f"Critical gaps: {result['summary']['critical_gaps']}")
|
| 475 |
+
print(f"Estimated cost: ${result['summary']['estimated_total_cost']:,}")
|
| 476 |
+
|
| 477 |
+
print(f"\nTop 3 gaps:")
|
| 478 |
+
for i, gap in enumerate(result['gaps'][:3], 1):
|
| 479 |
+
print(f"\n{i}. {gap['requirement']}")
|
| 480 |
+
print(f" Jurisdiction: {gap['jurisdiction'].upper()}")
|
| 481 |
+
print(f" Severity: {gap['severity']}")
|
| 482 |
+
print(f" Urgency: {gap['urgency']}")
|
| 483 |
+
print(f" Deadline: {gap['deadline']}")
|
| 484 |
+
print(f" Cost: ${gap['cost_estimate']['estimate']:,}")
|
| 485 |
+
|
| 486 |
+
print("\n=== Engine test complete! ===\n")
|
src/analysis/cost_calculator.py
ADDED
|
@@ -0,0 +1,544 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Cost Calculator for compliance cost estimation.
|
| 3 |
+
Migrated from tools.py to provide dedicated class with advanced features.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import logging
|
| 7 |
+
from typing import Dict, List, Optional
|
| 8 |
+
from datetime import datetime
|
| 9 |
+
|
| 10 |
+
logger = logging.getLogger(__name__)
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class CostCalculator:
|
| 14 |
+
"""
|
| 15 |
+
Calculate estimated compliance costs for crypto businesses.
|
| 16 |
+
|
| 17 |
+
Provides cost estimates across jurisdictions, activities, and time periods.
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
def __init__(self):
|
| 21 |
+
"""Initialize cost calculator with comprehensive cost database."""
|
| 22 |
+
self.cost_database = self._build_cost_database()
|
| 23 |
+
logger.info("CostCalculator initialized with cost database")
|
| 24 |
+
|
| 25 |
+
def _build_cost_database(self) -> Dict:
|
| 26 |
+
"""
|
| 27 |
+
Build comprehensive cost database.
|
| 28 |
+
|
| 29 |
+
Structure: {jurisdiction: {activity: {first_year, annual}}}
|
| 30 |
+
All costs in USD.
|
| 31 |
+
"""
|
| 32 |
+
return {
|
| 33 |
+
'us': {
|
| 34 |
+
'exchange': {
|
| 35 |
+
'first_year': (200000, 500000),
|
| 36 |
+
'annual': (100000, 250000),
|
| 37 |
+
'breakdown': {
|
| 38 |
+
'FinCEN MSB registration': (10000, 25000),
|
| 39 |
+
'State MTL licenses': (150000, 400000),
|
| 40 |
+
'Legal counsel': (25000, 50000),
|
| 41 |
+
'Compliance staff': (75000, 150000),
|
| 42 |
+
'AML/KYC systems': (50000, 100000)
|
| 43 |
+
}
|
| 44 |
+
},
|
| 45 |
+
'custody': {
|
| 46 |
+
'first_year': (100000, 300000),
|
| 47 |
+
'annual': (50000, 150000),
|
| 48 |
+
'breakdown': {
|
| 49 |
+
'Custody infrastructure': (50000, 150000),
|
| 50 |
+
'Insurance': (25000, 75000),
|
| 51 |
+
'Audits': (15000, 50000),
|
| 52 |
+
'Compliance program': (10000, 25000)
|
| 53 |
+
}
|
| 54 |
+
},
|
| 55 |
+
'staking': {
|
| 56 |
+
'first_year': (50000, 150000),
|
| 57 |
+
'annual': (25000, 75000),
|
| 58 |
+
'breakdown': {
|
| 59 |
+
'Legal analysis': (25000, 75000),
|
| 60 |
+
'Compliance monitoring': (15000, 50000),
|
| 61 |
+
'Disclosure requirements': (10000, 25000)
|
| 62 |
+
}
|
| 63 |
+
},
|
| 64 |
+
'lending': {
|
| 65 |
+
'first_year': (100000, 250000),
|
| 66 |
+
'annual': (50000, 125000),
|
| 67 |
+
'breakdown': {
|
| 68 |
+
'Securities review': (50000, 125000),
|
| 69 |
+
'State lending licenses': (30000, 75000),
|
| 70 |
+
'Compliance program': (20000, 50000)
|
| 71 |
+
}
|
| 72 |
+
},
|
| 73 |
+
'token_issuance': {
|
| 74 |
+
'first_year': (50000, 200000),
|
| 75 |
+
'annual': (25000, 100000),
|
| 76 |
+
'breakdown': {
|
| 77 |
+
'Legal counsel': (30000, 100000),
|
| 78 |
+
'SEC filing (if required)': (15000, 75000),
|
| 79 |
+
'Ongoing reporting': (5000, 25000)
|
| 80 |
+
}
|
| 81 |
+
},
|
| 82 |
+
'security_token_premium': {
|
| 83 |
+
'first_year': (200000, 500000),
|
| 84 |
+
'annual': (100000, 250000),
|
| 85 |
+
'breakdown': {
|
| 86 |
+
'SEC registration': (100000, 250000),
|
| 87 |
+
'Transfer agent': (30000, 75000),
|
| 88 |
+
'Legal opinions': (50000, 125000),
|
| 89 |
+
'Compliance officer': (20000, 50000)
|
| 90 |
+
}
|
| 91 |
+
},
|
| 92 |
+
'payment_processing': {
|
| 93 |
+
'first_year': (75000, 200000),
|
| 94 |
+
'annual': (40000, 100000),
|
| 95 |
+
'breakdown': {
|
| 96 |
+
'FinCEN registration': (10000, 20000),
|
| 97 |
+
'State licenses': (50000, 150000),
|
| 98 |
+
'AML compliance': (15000, 30000)
|
| 99 |
+
}
|
| 100 |
+
},
|
| 101 |
+
'mining': {
|
| 102 |
+
'first_year': (25000, 75000),
|
| 103 |
+
'annual': (15000, 40000),
|
| 104 |
+
'breakdown': {
|
| 105 |
+
'Energy regulation compliance': (15000, 50000),
|
| 106 |
+
'Tax reporting': (10000, 25000)
|
| 107 |
+
}
|
| 108 |
+
},
|
| 109 |
+
'nft_marketplace': {
|
| 110 |
+
'first_year': (40000, 100000),
|
| 111 |
+
'annual': (20000, 50000),
|
| 112 |
+
'breakdown': {
|
| 113 |
+
'IP compliance': (15000, 40000),
|
| 114 |
+
'Consumer protection': (15000, 40000),
|
| 115 |
+
'Payment processing': (10000, 20000)
|
| 116 |
+
}
|
| 117 |
+
},
|
| 118 |
+
'defi_protocol': {
|
| 119 |
+
'first_year': (75000, 250000),
|
| 120 |
+
'annual': (40000, 125000),
|
| 121 |
+
'breakdown': {
|
| 122 |
+
'Securities analysis': (50000, 150000),
|
| 123 |
+
'Smart contract audits': (15000, 75000),
|
| 124 |
+
'Legal counsel': (10000, 25000)
|
| 125 |
+
}
|
| 126 |
+
},
|
| 127 |
+
'base': {
|
| 128 |
+
'first_year': (50000, 100000),
|
| 129 |
+
'annual': (25000, 50000),
|
| 130 |
+
'breakdown': {
|
| 131 |
+
'General counsel': (25000, 50000),
|
| 132 |
+
'Compliance monitoring': (15000, 30000),
|
| 133 |
+
'Record keeping': (10000, 20000)
|
| 134 |
+
}
|
| 135 |
+
}
|
| 136 |
+
},
|
| 137 |
+
'eu': {
|
| 138 |
+
'exchange': {
|
| 139 |
+
'first_year': (150000, 400000),
|
| 140 |
+
'annual': (75000, 200000),
|
| 141 |
+
'breakdown': {
|
| 142 |
+
'MiCA CASP authorization': (100000, 300000),
|
| 143 |
+
'AMLD5 compliance': (30000, 75000),
|
| 144 |
+
'Legal counsel': (20000, 25000)
|
| 145 |
+
}
|
| 146 |
+
},
|
| 147 |
+
'custody': {
|
| 148 |
+
'first_year': (100000, 250000),
|
| 149 |
+
'annual': (50000, 125000),
|
| 150 |
+
'breakdown': {
|
| 151 |
+
'Safeguarding requirements': (50000, 150000),
|
| 152 |
+
'Insurance': (30000, 75000),
|
| 153 |
+
'Compliance program': (20000, 25000)
|
| 154 |
+
}
|
| 155 |
+
},
|
| 156 |
+
'staking': {
|
| 157 |
+
'first_year': (50000, 125000),
|
| 158 |
+
'annual': (25000, 60000),
|
| 159 |
+
'breakdown': {
|
| 160 |
+
'MiCA classification': (25000, 75000),
|
| 161 |
+
'Disclosure requirements': (15000, 35000),
|
| 162 |
+
'Ongoing monitoring': (10000, 15000)
|
| 163 |
+
}
|
| 164 |
+
},
|
| 165 |
+
'lending': {
|
| 166 |
+
'first_year': (75000, 200000),
|
| 167 |
+
'annual': (40000, 100000),
|
| 168 |
+
'breakdown': {
|
| 169 |
+
'MiCA compliance': (50000, 125000),
|
| 170 |
+
'Consumer credit rules': (15000, 50000),
|
| 171 |
+
'Legal counsel': (10000, 25000)
|
| 172 |
+
}
|
| 173 |
+
},
|
| 174 |
+
'token_issuance': {
|
| 175 |
+
'first_year': (100000, 300000),
|
| 176 |
+
'annual': (50000, 150000),
|
| 177 |
+
'breakdown': {
|
| 178 |
+
'White paper preparation': (50000, 150000),
|
| 179 |
+
'Regulatory notification': (30000, 100000),
|
| 180 |
+
'Ongoing disclosures': (20000, 50000)
|
| 181 |
+
}
|
| 182 |
+
},
|
| 183 |
+
'base': {
|
| 184 |
+
'first_year': (50000, 100000),
|
| 185 |
+
'annual': (25000, 50000),
|
| 186 |
+
'breakdown': {
|
| 187 |
+
'General compliance': (30000, 60000),
|
| 188 |
+
'Data protection (GDPR)': (20000, 40000)
|
| 189 |
+
}
|
| 190 |
+
}
|
| 191 |
+
},
|
| 192 |
+
'singapore': {
|
| 193 |
+
'exchange': {
|
| 194 |
+
'first_year': (150000, 350000),
|
| 195 |
+
'annual': (75000, 175000),
|
| 196 |
+
'breakdown': {
|
| 197 |
+
'MAS DPT license': (100000, 250000),
|
| 198 |
+
'Technology risk management': (30000, 75000),
|
| 199 |
+
'Compliance program': (20000, 25000)
|
| 200 |
+
}
|
| 201 |
+
},
|
| 202 |
+
'custody': {
|
| 203 |
+
'first_year': (75000, 200000),
|
| 204 |
+
'annual': (40000, 100000),
|
| 205 |
+
'breakdown': {
|
| 206 |
+
'Custody standards': (50000, 125000),
|
| 207 |
+
'Insurance/reserves': (15000, 50000),
|
| 208 |
+
'Audits': (10000, 25000)
|
| 209 |
+
}
|
| 210 |
+
},
|
| 211 |
+
'staking': {
|
| 212 |
+
'first_year': (40000, 100000),
|
| 213 |
+
'annual': (20000, 50000),
|
| 214 |
+
'breakdown': {
|
| 215 |
+
'MAS guidelines': (25000, 60000),
|
| 216 |
+
'Disclosure requirements': (10000, 30000),
|
| 217 |
+
'Ongoing compliance': (5000, 10000)
|
| 218 |
+
}
|
| 219 |
+
},
|
| 220 |
+
'lending': {
|
| 221 |
+
'first_year': (60000, 150000),
|
| 222 |
+
'annual': (30000, 75000),
|
| 223 |
+
'breakdown': {
|
| 224 |
+
'Regulatory assessment': (30000, 75000),
|
| 225 |
+
'Compliance program': (20000, 50000),
|
| 226 |
+
'Legal counsel': (10000, 25000)
|
| 227 |
+
}
|
| 228 |
+
},
|
| 229 |
+
'token_issuance': {
|
| 230 |
+
'first_year': (75000, 250000),
|
| 231 |
+
'annual': (40000, 125000),
|
| 232 |
+
'breakdown': {
|
| 233 |
+
'MAS exemption/license': (50000, 175000),
|
| 234 |
+
'Legal counsel': (15000, 50000),
|
| 235 |
+
'Prospectus preparation': (10000, 25000)
|
| 236 |
+
}
|
| 237 |
+
},
|
| 238 |
+
'base': {
|
| 239 |
+
'first_year': (40000, 80000),
|
| 240 |
+
'annual': (20000, 40000),
|
| 241 |
+
'breakdown': {
|
| 242 |
+
'General compliance': (25000, 50000),
|
| 243 |
+
'Tax advisory': (15000, 30000)
|
| 244 |
+
}
|
| 245 |
+
}
|
| 246 |
+
},
|
| 247 |
+
'uk': {
|
| 248 |
+
'exchange': {
|
| 249 |
+
'first_year': (125000, 300000),
|
| 250 |
+
'annual': (60000, 150000),
|
| 251 |
+
'breakdown': {
|
| 252 |
+
'FCA registration': (75000, 200000),
|
| 253 |
+
'AML/CTF compliance': (30000, 75000),
|
| 254 |
+
'Financial promotions': (20000, 25000)
|
| 255 |
+
}
|
| 256 |
+
},
|
| 257 |
+
'custody': {
|
| 258 |
+
'first_year': (75000, 200000),
|
| 259 |
+
'annual': (40000, 100000),
|
| 260 |
+
'breakdown': {
|
| 261 |
+
'Custody requirements': (50000, 125000),
|
| 262 |
+
'Client money rules': (15000, 50000),
|
| 263 |
+
'Audits': (10000, 25000)
|
| 264 |
+
}
|
| 265 |
+
},
|
| 266 |
+
'staking': {
|
| 267 |
+
'first_year': (40000, 100000),
|
| 268 |
+
'annual': (20000, 50000),
|
| 269 |
+
'breakdown': {
|
| 270 |
+
'FCA guidance': (20000, 50000),
|
| 271 |
+
'Disclosure requirements': (15000, 35000),
|
| 272 |
+
'Monitoring': (5000, 15000)
|
| 273 |
+
}
|
| 274 |
+
},
|
| 275 |
+
'lending': {
|
| 276 |
+
'first_year': (60000, 150000),
|
| 277 |
+
'annual': (30000, 75000),
|
| 278 |
+
'breakdown': {
|
| 279 |
+
'Consumer credit rules': (35000, 90000),
|
| 280 |
+
'FCA compliance': (15000, 40000),
|
| 281 |
+
'Legal counsel': (10000, 20000)
|
| 282 |
+
}
|
| 283 |
+
},
|
| 284 |
+
'token_issuance': {
|
| 285 |
+
'first_year': (50000, 150000),
|
| 286 |
+
'annual': (25000, 75000),
|
| 287 |
+
'breakdown': {
|
| 288 |
+
'Regulatory assessment': (30000, 90000),
|
| 289 |
+
'Promotions compliance': (15000, 45000),
|
| 290 |
+
'Legal opinions': (5000, 15000)
|
| 291 |
+
}
|
| 292 |
+
},
|
| 293 |
+
'base': {
|
| 294 |
+
'first_year': (40000, 75000),
|
| 295 |
+
'annual': (20000, 40000),
|
| 296 |
+
'breakdown': {
|
| 297 |
+
'General compliance': (25000, 50000),
|
| 298 |
+
'AML monitoring': (15000, 25000)
|
| 299 |
+
}
|
| 300 |
+
}
|
| 301 |
+
},
|
| 302 |
+
'uae': {
|
| 303 |
+
'exchange': {
|
| 304 |
+
'first_year': (100000, 250000),
|
| 305 |
+
'annual': (50000, 125000),
|
| 306 |
+
'breakdown': {
|
| 307 |
+
'VARA VASP license': (60000, 150000),
|
| 308 |
+
'Rulebook compliance': (25000, 75000),
|
| 309 |
+
'Compliance program': (15000, 25000)
|
| 310 |
+
}
|
| 311 |
+
},
|
| 312 |
+
'custody': {
|
| 313 |
+
'first_year': (60000, 150000),
|
| 314 |
+
'annual': (30000, 75000),
|
| 315 |
+
'breakdown': {
|
| 316 |
+
'VARA custody rules': (40000, 100000),
|
| 317 |
+
'Insurance': (15000, 35000),
|
| 318 |
+
'Monitoring': (5000, 15000)
|
| 319 |
+
}
|
| 320 |
+
},
|
| 321 |
+
'staking': {
|
| 322 |
+
'first_year': (30000, 80000),
|
| 323 |
+
'annual': (15000, 40000),
|
| 324 |
+
'breakdown': {
|
| 325 |
+
'VARA guidance': (20000, 50000),
|
| 326 |
+
'Disclosure': (10000, 25000)
|
| 327 |
+
}
|
| 328 |
+
},
|
| 329 |
+
'lending': {
|
| 330 |
+
'first_year': (50000, 125000),
|
| 331 |
+
'annual': (25000, 60000),
|
| 332 |
+
'breakdown': {
|
| 333 |
+
'Regulatory compliance': (30000, 75000),
|
| 334 |
+
'Legal counsel': (15000, 40000),
|
| 335 |
+
'Monitoring': (5000, 10000)
|
| 336 |
+
}
|
| 337 |
+
},
|
| 338 |
+
'token_issuance': {
|
| 339 |
+
'first_year': (50000, 150000),
|
| 340 |
+
'annual': (25000, 75000),
|
| 341 |
+
'breakdown': {
|
| 342 |
+
'VARA token rules': (35000, 100000),
|
| 343 |
+
'White paper': (10000, 40000),
|
| 344 |
+
'Ongoing disclosure': (5000, 10000)
|
| 345 |
+
}
|
| 346 |
+
},
|
| 347 |
+
'base': {
|
| 348 |
+
'first_year': (30000, 60000),
|
| 349 |
+
'annual': (15000, 30000),
|
| 350 |
+
'breakdown': {
|
| 351 |
+
'General compliance': (20000, 40000),
|
| 352 |
+
'AML/CTF': (10000, 20000)
|
| 353 |
+
}
|
| 354 |
+
}
|
| 355 |
+
}
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
+
def calculate_costs(
|
| 359 |
+
self,
|
| 360 |
+
jurisdictions: List[str],
|
| 361 |
+
activities: List[str],
|
| 362 |
+
is_security_token: bool = False,
|
| 363 |
+
years: int = 3
|
| 364 |
+
) -> Dict:
|
| 365 |
+
"""
|
| 366 |
+
Calculate comprehensive cost estimates.
|
| 367 |
+
|
| 368 |
+
Args:
|
| 369 |
+
jurisdictions: List of jurisdictions
|
| 370 |
+
activities: List of activities
|
| 371 |
+
is_security_token: Whether token is a security
|
| 372 |
+
years: Number of years to project (default 3)
|
| 373 |
+
|
| 374 |
+
Returns:
|
| 375 |
+
Dictionary with cost breakdown
|
| 376 |
+
"""
|
| 377 |
+
results = {}
|
| 378 |
+
|
| 379 |
+
for jurisdiction in jurisdictions:
|
| 380 |
+
if jurisdiction not in self.cost_database:
|
| 381 |
+
logger.warning(f"No cost data for jurisdiction: {jurisdiction}")
|
| 382 |
+
continue
|
| 383 |
+
|
| 384 |
+
costs = self.cost_database[jurisdiction]
|
| 385 |
+
|
| 386 |
+
# Initialize totals
|
| 387 |
+
first_year_min = 0
|
| 388 |
+
first_year_max = 0
|
| 389 |
+
annual_min = 0
|
| 390 |
+
annual_max = 0
|
| 391 |
+
all_breakdowns = []
|
| 392 |
+
|
| 393 |
+
# Base costs
|
| 394 |
+
if 'base' in costs:
|
| 395 |
+
first_year_min += costs['base']['first_year'][0]
|
| 396 |
+
first_year_max += costs['base']['first_year'][1]
|
| 397 |
+
annual_min += costs['base']['annual'][0]
|
| 398 |
+
annual_max += costs['base']['annual'][1]
|
| 399 |
+
all_breakdowns.append({
|
| 400 |
+
'category': 'Base Compliance',
|
| 401 |
+
'first_year': costs['base']['first_year'],
|
| 402 |
+
'annual': costs['base']['annual'],
|
| 403 |
+
'breakdown': costs['base'].get('breakdown', {})
|
| 404 |
+
})
|
| 405 |
+
|
| 406 |
+
# Activity-specific costs
|
| 407 |
+
for activity in activities:
|
| 408 |
+
if activity in costs:
|
| 409 |
+
first_year_min += costs[activity]['first_year'][0]
|
| 410 |
+
first_year_max += costs[activity]['first_year'][1]
|
| 411 |
+
annual_min += costs[activity]['annual'][0]
|
| 412 |
+
annual_max += costs[activity]['annual'][1]
|
| 413 |
+
all_breakdowns.append({
|
| 414 |
+
'category': activity.replace('_', ' ').title(),
|
| 415 |
+
'first_year': costs[activity]['first_year'],
|
| 416 |
+
'annual': costs[activity]['annual'],
|
| 417 |
+
'breakdown': costs[activity].get('breakdown', {})
|
| 418 |
+
})
|
| 419 |
+
|
| 420 |
+
# Security token premium (US only)
|
| 421 |
+
if is_security_token and jurisdiction == 'us' and 'security_token_premium' in costs:
|
| 422 |
+
premium = costs['security_token_premium']
|
| 423 |
+
first_year_min += premium['first_year'][0]
|
| 424 |
+
first_year_max += premium['first_year'][1]
|
| 425 |
+
annual_min += premium['annual'][0]
|
| 426 |
+
annual_max += premium['annual'][1]
|
| 427 |
+
all_breakdowns.append({
|
| 428 |
+
'category': 'Security Token Premium',
|
| 429 |
+
'first_year': premium['first_year'],
|
| 430 |
+
'annual': premium['annual'],
|
| 431 |
+
'breakdown': premium.get('breakdown', {})
|
| 432 |
+
})
|
| 433 |
+
|
| 434 |
+
# Multi-year projection
|
| 435 |
+
multi_year = []
|
| 436 |
+
for year in range(1, years + 1):
|
| 437 |
+
if year == 1:
|
| 438 |
+
year_min = first_year_min
|
| 439 |
+
year_max = first_year_max
|
| 440 |
+
else:
|
| 441 |
+
year_min = annual_min
|
| 442 |
+
year_max = annual_max
|
| 443 |
+
|
| 444 |
+
multi_year.append({
|
| 445 |
+
'year': year,
|
| 446 |
+
'min': year_min,
|
| 447 |
+
'max': year_max,
|
| 448 |
+
'estimate': (year_min + year_max) // 2
|
| 449 |
+
})
|
| 450 |
+
|
| 451 |
+
# Calculate totals
|
| 452 |
+
total_min = first_year_min + (annual_min * (years - 1))
|
| 453 |
+
total_max = first_year_max + (annual_max * (years - 1))
|
| 454 |
+
|
| 455 |
+
results[jurisdiction] = {
|
| 456 |
+
'first_year': {
|
| 457 |
+
'min': first_year_min,
|
| 458 |
+
'max': first_year_max,
|
| 459 |
+
'estimate': (first_year_min + first_year_max) // 2
|
| 460 |
+
},
|
| 461 |
+
'annual_ongoing': {
|
| 462 |
+
'min': annual_min,
|
| 463 |
+
'max': annual_max,
|
| 464 |
+
'estimate': (annual_min + annual_max) // 2
|
| 465 |
+
},
|
| 466 |
+
f'total_{years}_years': {
|
| 467 |
+
'min': total_min,
|
| 468 |
+
'max': total_max,
|
| 469 |
+
'estimate': (total_min + total_max) // 2
|
| 470 |
+
},
|
| 471 |
+
'breakdown': all_breakdowns,
|
| 472 |
+
'multi_year_projection': multi_year
|
| 473 |
+
}
|
| 474 |
+
|
| 475 |
+
# Calculate grand total
|
| 476 |
+
grand_total_first = sum(
|
| 477 |
+
j['first_year']['estimate'] for j in results.values()
|
| 478 |
+
)
|
| 479 |
+
grand_total_annual = sum(
|
| 480 |
+
j['annual_ongoing']['estimate'] for j in results.values()
|
| 481 |
+
)
|
| 482 |
+
grand_total_multi = sum(
|
| 483 |
+
j[f'total_{years}_years']['estimate'] for j in results.values()
|
| 484 |
+
)
|
| 485 |
+
|
| 486 |
+
logger.info(
|
| 487 |
+
f"Cost calculation: {len(results)} jurisdictions, "
|
| 488 |
+
f"first year: ${grand_total_first:,}, "
|
| 489 |
+
f"{years}-year total: ${grand_total_multi:,}"
|
| 490 |
+
)
|
| 491 |
+
|
| 492 |
+
return {
|
| 493 |
+
'by_jurisdiction': results,
|
| 494 |
+
'grand_totals': {
|
| 495 |
+
'first_year': grand_total_first,
|
| 496 |
+
'annual_ongoing': grand_total_annual,
|
| 497 |
+
f'total_{years}_years': grand_total_multi
|
| 498 |
+
},
|
| 499 |
+
'parameters': {
|
| 500 |
+
'jurisdictions': jurisdictions,
|
| 501 |
+
'activities': activities,
|
| 502 |
+
'is_security_token': is_security_token,
|
| 503 |
+
'projection_years': years
|
| 504 |
+
},
|
| 505 |
+
'calculated_at': datetime.now().isoformat()
|
| 506 |
+
}
|
| 507 |
+
|
| 508 |
+
|
| 509 |
+
# Convenience function
|
| 510 |
+
def calculate_costs(
|
| 511 |
+
jurisdictions: List[str],
|
| 512 |
+
activities: List[str],
|
| 513 |
+
is_security_token: bool = False,
|
| 514 |
+
years: int = 3
|
| 515 |
+
) -> Dict:
|
| 516 |
+
"""Quick cost calculation."""
|
| 517 |
+
calculator = CostCalculator()
|
| 518 |
+
return calculator.calculate_costs(jurisdictions, activities, is_security_token, years)
|
| 519 |
+
|
| 520 |
+
|
| 521 |
+
if __name__ == "__main__":
|
| 522 |
+
# Test the calculator
|
| 523 |
+
print("\n=== Testing Cost Calculator ===\n")
|
| 524 |
+
|
| 525 |
+
result = calculate_costs(
|
| 526 |
+
jurisdictions=['us', 'eu', 'singapore'],
|
| 527 |
+
activities=['exchange', 'custody'],
|
| 528 |
+
is_security_token=False,
|
| 529 |
+
years=3
|
| 530 |
+
)
|
| 531 |
+
|
| 532 |
+
print(f"Grand Totals:")
|
| 533 |
+
print(f" First year: ${result['grand_totals']['first_year']:,}")
|
| 534 |
+
print(f" Annual ongoing: ${result['grand_totals']['annual_ongoing']:,}")
|
| 535 |
+
print(f" 3-year total: ${result['grand_totals']['total_3_years']:,}")
|
| 536 |
+
|
| 537 |
+
print(f"\nBy Jurisdiction:")
|
| 538 |
+
for jur, data in result['by_jurisdiction'].items():
|
| 539 |
+
print(f"\n{jur.upper()}:")
|
| 540 |
+
print(f" First year: ${data['first_year']['estimate']:,}")
|
| 541 |
+
print(f" Annual: ${data['annual_ongoing']['estimate']:,}")
|
| 542 |
+
print(f" 3-year total: ${data['total_3_years']['estimate']:,}")
|
| 543 |
+
|
| 544 |
+
print("\n=== Calculator test complete! ===\n")
|
src/analysis/risk_scorer.py
ADDED
|
@@ -0,0 +1,454 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Risk Scorer with configurable weights for compliance risk assessment.
|
| 3 |
+
Provides structured, explainable risk scoring from 0-100.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import logging
|
| 7 |
+
from typing import Dict, List, Optional
|
| 8 |
+
from datetime import datetime
|
| 9 |
+
from enum import Enum
|
| 10 |
+
|
| 11 |
+
logger = logging.getLogger(__name__)
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class RiskLevel(Enum):
|
| 15 |
+
"""Risk level categories."""
|
| 16 |
+
LOW = "low" # 0-40
|
| 17 |
+
MEDIUM = "medium" # 41-70
|
| 18 |
+
HIGH = "high" # 71-85
|
| 19 |
+
CRITICAL = "critical" # 86-100
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class RiskFactor:
|
| 23 |
+
"""Represents a single risk factor contribution."""
|
| 24 |
+
|
| 25 |
+
def __init__(
|
| 26 |
+
self,
|
| 27 |
+
name: str,
|
| 28 |
+
score: float,
|
| 29 |
+
weight: float,
|
| 30 |
+
weighted_score: float,
|
| 31 |
+
description: str
|
| 32 |
+
):
|
| 33 |
+
self.name = name
|
| 34 |
+
self.score = score # 0-100 for this factor
|
| 35 |
+
self.weight = weight # Weight in final score
|
| 36 |
+
self.weighted_score = weighted_score # score * weight
|
| 37 |
+
self.description = description
|
| 38 |
+
|
| 39 |
+
def to_dict(self) -> Dict:
|
| 40 |
+
"""Convert to dictionary."""
|
| 41 |
+
return {
|
| 42 |
+
'name': self.name,
|
| 43 |
+
'score': round(self.score, 2),
|
| 44 |
+
'weight': round(self.weight, 2),
|
| 45 |
+
'weighted_score': round(self.weighted_score, 2),
|
| 46 |
+
'description': self.description
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
class RiskScorer:
|
| 51 |
+
"""
|
| 52 |
+
Configurable risk scorer for compliance analysis.
|
| 53 |
+
|
| 54 |
+
Calculates risk score (0-100) based on multiple weighted factors.
|
| 55 |
+
"""
|
| 56 |
+
|
| 57 |
+
def __init__(self, custom_weights: Optional[Dict[str, float]] = None):
|
| 58 |
+
"""
|
| 59 |
+
Initialize risk scorer.
|
| 60 |
+
|
| 61 |
+
Args:
|
| 62 |
+
custom_weights: Optional custom weights for factors
|
| 63 |
+
Format: {'gap_severity': 0.35, 'gap_count': 0.25, ...}
|
| 64 |
+
"""
|
| 65 |
+
# Default weights (must sum to 1.0)
|
| 66 |
+
self.weights = {
|
| 67 |
+
'gap_severity': 0.35, # Severity of compliance gaps
|
| 68 |
+
'gap_count': 0.25, # Number of gaps
|
| 69 |
+
'token_classification': 0.15, # Security token risk
|
| 70 |
+
'urgency': 0.15, # Deadline urgency
|
| 71 |
+
'enforcement_history': 0.10 # Jurisdiction enforcement risk
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
# Override with custom weights if provided
|
| 75 |
+
if custom_weights:
|
| 76 |
+
self.weights.update(custom_weights)
|
| 77 |
+
|
| 78 |
+
# Normalize weights to sum to 1.0
|
| 79 |
+
total_weight = sum(self.weights.values())
|
| 80 |
+
if total_weight != 1.0:
|
| 81 |
+
logger.warning(f"Weights sum to {total_weight}, normalizing to 1.0")
|
| 82 |
+
self.weights = {k: v / total_weight for k, v in self.weights.items()}
|
| 83 |
+
|
| 84 |
+
# Enforcement risk scores by jurisdiction (0-100)
|
| 85 |
+
self.enforcement_risk = {
|
| 86 |
+
'us': 85, # US: High enforcement, especially SEC
|
| 87 |
+
'eu': 70, # EU: Moderate-high, MiCA implementation
|
| 88 |
+
'singapore': 60, # Singapore: Moderate, clear framework
|
| 89 |
+
'uk': 65, # UK: Moderate, post-Brexit evolution
|
| 90 |
+
'uae': 50 # UAE: Moderate-low, emerging framework
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
logger.info(f"RiskScorer initialized with weights: {self.weights}")
|
| 94 |
+
|
| 95 |
+
def calculate_risk(
|
| 96 |
+
self,
|
| 97 |
+
gaps: List[Dict],
|
| 98 |
+
token_classification: Optional[Dict] = None,
|
| 99 |
+
jurisdictions: Optional[List[str]] = None
|
| 100 |
+
) -> Dict:
|
| 101 |
+
"""
|
| 102 |
+
Calculate overall risk score from compliance gaps.
|
| 103 |
+
|
| 104 |
+
Args:
|
| 105 |
+
gaps: List of compliance gaps from ComplianceEngine
|
| 106 |
+
token_classification: Token classification result (if applicable)
|
| 107 |
+
jurisdictions: List of jurisdictions being analyzed
|
| 108 |
+
|
| 109 |
+
Returns:
|
| 110 |
+
Dictionary with risk score, level, and breakdown
|
| 111 |
+
"""
|
| 112 |
+
factors = []
|
| 113 |
+
|
| 114 |
+
# Factor 1: Gap Severity
|
| 115 |
+
severity_score, severity_desc = self._score_gap_severity(gaps)
|
| 116 |
+
factors.append(RiskFactor(
|
| 117 |
+
name='Gap Severity',
|
| 118 |
+
score=severity_score,
|
| 119 |
+
weight=self.weights['gap_severity'],
|
| 120 |
+
weighted_score=severity_score * self.weights['gap_severity'],
|
| 121 |
+
description=severity_desc
|
| 122 |
+
))
|
| 123 |
+
|
| 124 |
+
# Factor 2: Gap Count
|
| 125 |
+
count_score, count_desc = self._score_gap_count(gaps)
|
| 126 |
+
factors.append(RiskFactor(
|
| 127 |
+
name='Gap Count',
|
| 128 |
+
score=count_score,
|
| 129 |
+
weight=self.weights['gap_count'],
|
| 130 |
+
weighted_score=count_score * self.weights['gap_count'],
|
| 131 |
+
description=count_desc
|
| 132 |
+
))
|
| 133 |
+
|
| 134 |
+
# Factor 3: Token Classification
|
| 135 |
+
token_score, token_desc = self._score_token_classification(token_classification)
|
| 136 |
+
factors.append(RiskFactor(
|
| 137 |
+
name='Token Classification',
|
| 138 |
+
score=token_score,
|
| 139 |
+
weight=self.weights['token_classification'],
|
| 140 |
+
weighted_score=token_score * self.weights['token_classification'],
|
| 141 |
+
description=token_desc
|
| 142 |
+
))
|
| 143 |
+
|
| 144 |
+
# Factor 4: Urgency
|
| 145 |
+
urgency_score, urgency_desc = self._score_urgency(gaps)
|
| 146 |
+
factors.append(RiskFactor(
|
| 147 |
+
name='Urgency',
|
| 148 |
+
score=urgency_score,
|
| 149 |
+
weight=self.weights['urgency'],
|
| 150 |
+
weighted_score=urgency_score * self.weights['urgency'],
|
| 151 |
+
description=urgency_desc
|
| 152 |
+
))
|
| 153 |
+
|
| 154 |
+
# Factor 5: Enforcement History
|
| 155 |
+
enforcement_score, enforcement_desc = self._score_enforcement(jurisdictions or [])
|
| 156 |
+
factors.append(RiskFactor(
|
| 157 |
+
name='Enforcement Risk',
|
| 158 |
+
score=enforcement_score,
|
| 159 |
+
weight=self.weights['enforcement_history'],
|
| 160 |
+
weighted_score=enforcement_score * self.weights['enforcement_history'],
|
| 161 |
+
description=enforcement_desc
|
| 162 |
+
))
|
| 163 |
+
|
| 164 |
+
# Calculate total weighted score
|
| 165 |
+
total_score = sum(f.weighted_score for f in factors)
|
| 166 |
+
total_score = min(max(total_score, 0), 100) # Clamp to 0-100
|
| 167 |
+
|
| 168 |
+
# Determine risk level
|
| 169 |
+
risk_level = self._get_risk_level(total_score)
|
| 170 |
+
|
| 171 |
+
result = {
|
| 172 |
+
'risk_score': round(total_score, 2),
|
| 173 |
+
'risk_level': risk_level.value,
|
| 174 |
+
'risk_category': risk_level.name,
|
| 175 |
+
'factors': [f.to_dict() for f in factors],
|
| 176 |
+
'summary': self._generate_summary(total_score, risk_level, factors),
|
| 177 |
+
'recommendations': self._generate_risk_recommendations(total_score, factors),
|
| 178 |
+
'calculated_at': datetime.now().isoformat()
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
logger.info(
|
| 182 |
+
f"Risk calculated: {total_score:.2f} ({risk_level.value}) from "
|
| 183 |
+
f"{len(gaps)} gaps, {len(jurisdictions or [])} jurisdictions"
|
| 184 |
+
)
|
| 185 |
+
|
| 186 |
+
return result
|
| 187 |
+
|
| 188 |
+
def _score_gap_severity(self, gaps: List[Dict]) -> tuple[float, str]:
|
| 189 |
+
"""Score based on gap severity distribution."""
|
| 190 |
+
if not gaps:
|
| 191 |
+
return 0.0, "No compliance gaps identified"
|
| 192 |
+
|
| 193 |
+
severity_weights = {
|
| 194 |
+
'critical': 100,
|
| 195 |
+
'high': 75,
|
| 196 |
+
'medium': 50,
|
| 197 |
+
'low': 25
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
# Calculate weighted average severity
|
| 201 |
+
total_weight = 0
|
| 202 |
+
weighted_sum = 0
|
| 203 |
+
|
| 204 |
+
severity_counts = {'critical': 0, 'high': 0, 'medium': 0, 'low': 0}
|
| 205 |
+
|
| 206 |
+
for gap in gaps:
|
| 207 |
+
severity = gap.get('severity', 'medium')
|
| 208 |
+
severity_counts[severity] = severity_counts.get(severity, 0) + 1
|
| 209 |
+
weight = severity_weights.get(severity, 50)
|
| 210 |
+
weighted_sum += weight
|
| 211 |
+
total_weight += 1
|
| 212 |
+
|
| 213 |
+
score = weighted_sum / total_weight if total_weight > 0 else 0
|
| 214 |
+
|
| 215 |
+
desc = f"{severity_counts['critical']} critical, {severity_counts['high']} high, " \
|
| 216 |
+
f"{severity_counts['medium']} medium, {severity_counts['low']} low severity gaps"
|
| 217 |
+
|
| 218 |
+
return score, desc
|
| 219 |
+
|
| 220 |
+
def _score_gap_count(self, gaps: List[Dict]) -> tuple[float, str]:
|
| 221 |
+
"""Score based on number of gaps."""
|
| 222 |
+
count = len(gaps)
|
| 223 |
+
|
| 224 |
+
# Score curve: 0 gaps = 0, 1-2 = 20, 3-4 = 40, 5-6 = 60, 7-8 = 80, 9+ = 100
|
| 225 |
+
if count == 0:
|
| 226 |
+
score = 0
|
| 227 |
+
elif count <= 2:
|
| 228 |
+
score = count * 10
|
| 229 |
+
elif count <= 4:
|
| 230 |
+
score = 20 + (count - 2) * 10
|
| 231 |
+
elif count <= 6:
|
| 232 |
+
score = 40 + (count - 4) * 10
|
| 233 |
+
elif count <= 8:
|
| 234 |
+
score = 60 + (count - 6) * 10
|
| 235 |
+
else:
|
| 236 |
+
score = min(80 + (count - 8) * 5, 100)
|
| 237 |
+
|
| 238 |
+
desc = f"{count} compliance gaps require remediation"
|
| 239 |
+
|
| 240 |
+
return score, desc
|
| 241 |
+
|
| 242 |
+
def _score_token_classification(
|
| 243 |
+
self,
|
| 244 |
+
classification: Optional[Dict]
|
| 245 |
+
) -> tuple[float, str]:
|
| 246 |
+
"""Score based on token classification."""
|
| 247 |
+
if not classification:
|
| 248 |
+
return 0.0, "No token issuance"
|
| 249 |
+
|
| 250 |
+
# Check if classified as security in any jurisdiction
|
| 251 |
+
is_security = False
|
| 252 |
+
security_jurisdictions = []
|
| 253 |
+
|
| 254 |
+
for jurisdiction, result in classification.items():
|
| 255 |
+
if result.get('classification') in ['security', 'capital markets product']:
|
| 256 |
+
is_security = True
|
| 257 |
+
security_jurisdictions.append(jurisdiction.upper())
|
| 258 |
+
|
| 259 |
+
if is_security:
|
| 260 |
+
score = 90.0 # Very high risk for unregistered securities
|
| 261 |
+
desc = f"Token classified as security in {', '.join(security_jurisdictions)}"
|
| 262 |
+
else:
|
| 263 |
+
score = 20.0 # Still some risk for utility tokens
|
| 264 |
+
desc = "Token classified as utility/non-security"
|
| 265 |
+
|
| 266 |
+
return score, desc
|
| 267 |
+
|
| 268 |
+
def _score_urgency(self, gaps: List[Dict]) -> tuple[float, str]:
|
| 269 |
+
"""Score based on deadline urgency."""
|
| 270 |
+
if not gaps:
|
| 271 |
+
return 0.0, "No urgent deadlines"
|
| 272 |
+
|
| 273 |
+
urgency_weights = {
|
| 274 |
+
'immediate': 100,
|
| 275 |
+
'urgent': 75,
|
| 276 |
+
'moderate': 50,
|
| 277 |
+
'planning': 25
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
# Calculate weighted average urgency
|
| 281 |
+
total_weight = 0
|
| 282 |
+
weighted_sum = 0
|
| 283 |
+
|
| 284 |
+
urgency_counts = {'immediate': 0, 'urgent': 0, 'moderate': 0, 'planning': 0}
|
| 285 |
+
|
| 286 |
+
for gap in gaps:
|
| 287 |
+
urgency = gap.get('urgency', 'moderate')
|
| 288 |
+
urgency_counts[urgency] = urgency_counts.get(urgency, 0) + 1
|
| 289 |
+
weight = urgency_weights.get(urgency, 50)
|
| 290 |
+
weighted_sum += weight
|
| 291 |
+
total_weight += 1
|
| 292 |
+
|
| 293 |
+
score = weighted_sum / total_weight if total_weight > 0 else 0
|
| 294 |
+
|
| 295 |
+
desc = f"{urgency_counts['immediate']} immediate, {urgency_counts['urgent']} urgent deadlines"
|
| 296 |
+
|
| 297 |
+
return score, desc
|
| 298 |
+
|
| 299 |
+
def _score_enforcement(self, jurisdictions: List[str]) -> tuple[float, str]:
|
| 300 |
+
"""Score based on enforcement risk of jurisdictions."""
|
| 301 |
+
if not jurisdictions:
|
| 302 |
+
return 0.0, "No jurisdictions specified"
|
| 303 |
+
|
| 304 |
+
# Average enforcement risk across jurisdictions
|
| 305 |
+
enforcement_scores = [
|
| 306 |
+
self.enforcement_risk.get(j, 50) for j in jurisdictions
|
| 307 |
+
]
|
| 308 |
+
|
| 309 |
+
score = sum(enforcement_scores) / len(enforcement_scores)
|
| 310 |
+
|
| 311 |
+
highest_risk = max(jurisdictions, key=lambda j: self.enforcement_risk.get(j, 0))
|
| 312 |
+
|
| 313 |
+
desc = f"Operating in {len(jurisdictions)} jurisdictions, highest risk: {highest_risk.upper()}"
|
| 314 |
+
|
| 315 |
+
return score, desc
|
| 316 |
+
|
| 317 |
+
def _get_risk_level(self, score: float) -> RiskLevel:
|
| 318 |
+
"""Determine risk level from score."""
|
| 319 |
+
if score >= 86:
|
| 320 |
+
return RiskLevel.CRITICAL
|
| 321 |
+
elif score >= 71:
|
| 322 |
+
return RiskLevel.HIGH
|
| 323 |
+
elif score >= 41:
|
| 324 |
+
return RiskLevel.MEDIUM
|
| 325 |
+
else:
|
| 326 |
+
return RiskLevel.LOW
|
| 327 |
+
|
| 328 |
+
def _generate_summary(
|
| 329 |
+
self,
|
| 330 |
+
score: float,
|
| 331 |
+
level: RiskLevel,
|
| 332 |
+
factors: List[RiskFactor]
|
| 333 |
+
) -> str:
|
| 334 |
+
"""Generate human-readable summary."""
|
| 335 |
+
top_factor = max(factors, key=lambda f: f.weighted_score)
|
| 336 |
+
|
| 337 |
+
if level == RiskLevel.CRITICAL:
|
| 338 |
+
return f"CRITICAL RISK ({score:.0f}/100): Immediate action required. " \
|
| 339 |
+
f"Primary risk: {top_factor.name} ({top_factor.score:.0f}/100)."
|
| 340 |
+
elif level == RiskLevel.HIGH:
|
| 341 |
+
return f"HIGH RISK ({score:.0f}/100): Urgent compliance measures needed. " \
|
| 342 |
+
f"Primary concern: {top_factor.name}."
|
| 343 |
+
elif level == RiskLevel.MEDIUM:
|
| 344 |
+
return f"MEDIUM RISK ({score:.0f}/100): Compliance improvements recommended. " \
|
| 345 |
+
f"Focus on: {top_factor.name}."
|
| 346 |
+
else:
|
| 347 |
+
return f"LOW RISK ({score:.0f}/100): Minimal compliance concerns. " \
|
| 348 |
+
f"Continue monitoring: {top_factor.name}."
|
| 349 |
+
|
| 350 |
+
def _generate_risk_recommendations(
|
| 351 |
+
self,
|
| 352 |
+
score: float,
|
| 353 |
+
factors: List[RiskFactor]
|
| 354 |
+
) -> List[str]:
|
| 355 |
+
"""Generate recommendations based on risk factors."""
|
| 356 |
+
recommendations = []
|
| 357 |
+
|
| 358 |
+
# Sort factors by weighted contribution
|
| 359 |
+
sorted_factors = sorted(factors, key=lambda f: f.weighted_score, reverse=True)
|
| 360 |
+
|
| 361 |
+
for factor in sorted_factors:
|
| 362 |
+
if factor.weighted_score > 15: # Significant contributor
|
| 363 |
+
if factor.name == 'Gap Severity':
|
| 364 |
+
recommendations.append(
|
| 365 |
+
"Address critical severity gaps immediately with legal counsel"
|
| 366 |
+
)
|
| 367 |
+
elif factor.name == 'Gap Count':
|
| 368 |
+
recommendations.append(
|
| 369 |
+
"Develop systematic compliance roadmap to address multiple gaps"
|
| 370 |
+
)
|
| 371 |
+
elif factor.name == 'Token Classification':
|
| 372 |
+
recommendations.append(
|
| 373 |
+
"Consult securities lawyer immediately for token registration strategy"
|
| 374 |
+
)
|
| 375 |
+
elif factor.name == 'Urgency':
|
| 376 |
+
recommendations.append(
|
| 377 |
+
"Prioritize urgent deadlines to avoid enforcement action"
|
| 378 |
+
)
|
| 379 |
+
elif factor.name == 'Enforcement Risk':
|
| 380 |
+
recommendations.append(
|
| 381 |
+
"Consider jurisdiction risk in business strategy and timeline"
|
| 382 |
+
)
|
| 383 |
+
|
| 384 |
+
# Overall recommendation based on score
|
| 385 |
+
if score >= 71:
|
| 386 |
+
recommendations.insert(
|
| 387 |
+
0,
|
| 388 |
+
"URGENT: Engage compliance counsel and consider delaying launch until gaps addressed"
|
| 389 |
+
)
|
| 390 |
+
elif score >= 41:
|
| 391 |
+
recommendations.insert(
|
| 392 |
+
0,
|
| 393 |
+
"Develop 90-day compliance action plan with clear milestones"
|
| 394 |
+
)
|
| 395 |
+
|
| 396 |
+
return recommendations[:5] # Top 5 recommendations
|
| 397 |
+
|
| 398 |
+
|
| 399 |
+
# Convenience function
|
| 400 |
+
def calculate_risk(
|
| 401 |
+
gaps: List[Dict],
|
| 402 |
+
token_classification: Optional[Dict] = None,
|
| 403 |
+
jurisdictions: Optional[List[str]] = None
|
| 404 |
+
) -> Dict:
|
| 405 |
+
"""Quick risk calculation."""
|
| 406 |
+
scorer = RiskScorer()
|
| 407 |
+
return scorer.calculate_risk(gaps, token_classification, jurisdictions)
|
| 408 |
+
|
| 409 |
+
|
| 410 |
+
if __name__ == "__main__":
|
| 411 |
+
# Test the scorer
|
| 412 |
+
print("\n=== Testing Risk Scorer ===\n")
|
| 413 |
+
|
| 414 |
+
sample_gaps = [
|
| 415 |
+
{
|
| 416 |
+
'requirement': 'FinCEN MSB Registration',
|
| 417 |
+
'severity': 'critical',
|
| 418 |
+
'urgency': 'immediate',
|
| 419 |
+
'jurisdiction': 'us'
|
| 420 |
+
},
|
| 421 |
+
{
|
| 422 |
+
'requirement': 'State MTL Licenses',
|
| 423 |
+
'severity': 'critical',
|
| 424 |
+
'urgency': 'urgent',
|
| 425 |
+
'jurisdiction': 'us'
|
| 426 |
+
},
|
| 427 |
+
{
|
| 428 |
+
'requirement': 'MiCA CASP Authorization',
|
| 429 |
+
'severity': 'high',
|
| 430 |
+
'urgency': 'moderate',
|
| 431 |
+
'jurisdiction': 'eu'
|
| 432 |
+
}
|
| 433 |
+
]
|
| 434 |
+
|
| 435 |
+
result = calculate_risk(
|
| 436 |
+
gaps=sample_gaps,
|
| 437 |
+
token_classification={'us': {'classification': 'security'}},
|
| 438 |
+
jurisdictions=['us', 'eu']
|
| 439 |
+
)
|
| 440 |
+
|
| 441 |
+
print(f"Risk Score: {result['risk_score']}/100")
|
| 442 |
+
print(f"Risk Level: {result['risk_level'].upper()}")
|
| 443 |
+
print(f"\nSummary: {result['summary']}")
|
| 444 |
+
|
| 445 |
+
print(f"\nRisk Factors:")
|
| 446 |
+
for factor in result['factors']:
|
| 447 |
+
print(f" - {factor['name']}: {factor['score']:.0f}/100 "
|
| 448 |
+
f"(weight: {factor['weight']:.0%}, contribution: {factor['weighted_score']:.1f})")
|
| 449 |
+
|
| 450 |
+
print(f"\nRecommendations:")
|
| 451 |
+
for i, rec in enumerate(result['recommendations'], 1):
|
| 452 |
+
print(f" {i}. {rec}")
|
| 453 |
+
|
| 454 |
+
print("\n=== Scorer test complete! ===\n")
|
src/analysis/token_classifier.py
ADDED
|
@@ -0,0 +1,487 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Token classification using the Howey Test and other regulatory frameworks.
|
| 3 |
+
Determines if a crypto token is a security or utility token.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import logging
|
| 7 |
+
from typing import Dict, List, Optional, Tuple
|
| 8 |
+
from datetime import datetime
|
| 9 |
+
import re
|
| 10 |
+
|
| 11 |
+
logger = logging.getLogger(__name__)
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class HoweyTestAnalyzer:
|
| 15 |
+
"""
|
| 16 |
+
Analyzes tokens using the SEC's Howey Test.
|
| 17 |
+
|
| 18 |
+
The Howey Test has 4 prongs:
|
| 19 |
+
1. Investment of money
|
| 20 |
+
2. In a common enterprise
|
| 21 |
+
3. With an expectation of profits
|
| 22 |
+
4. Derived from the efforts of others
|
| 23 |
+
|
| 24 |
+
If all 4 are met, the token is likely a security.
|
| 25 |
+
"""
|
| 26 |
+
|
| 27 |
+
def __init__(self):
|
| 28 |
+
"""Initialize Howey Test analyzer."""
|
| 29 |
+
self.test_criteria = {
|
| 30 |
+
'investment_of_money': {
|
| 31 |
+
'keywords': [
|
| 32 |
+
'purchase', 'buy', 'invest', 'sale', 'ico', 'token sale',
|
| 33 |
+
'presale', 'crowdsale', 'fundraising', 'payment', 'contribute'
|
| 34 |
+
],
|
| 35 |
+
'weight': 0.25
|
| 36 |
+
},
|
| 37 |
+
'common_enterprise': {
|
| 38 |
+
'keywords': [
|
| 39 |
+
'pool', 'pooled', 'combined', 'collective', 'together',
|
| 40 |
+
'treasury', 'ecosystem', 'platform', 'network', 'protocol'
|
| 41 |
+
],
|
| 42 |
+
'weight': 0.25
|
| 43 |
+
},
|
| 44 |
+
'expectation_of_profits': {
|
| 45 |
+
'keywords': [
|
| 46 |
+
'profit', 'returns', 'gains', 'appreciation', 'yield',
|
| 47 |
+
'rewards', 'earnings', 'income', 'dividend', 'interest',
|
| 48 |
+
'roi', 'return on investment', 'price increase'
|
| 49 |
+
],
|
| 50 |
+
'weight': 0.25
|
| 51 |
+
},
|
| 52 |
+
'efforts_of_others': {
|
| 53 |
+
'keywords': [
|
| 54 |
+
'team', 'development', 'management', 'founders', 'developers',
|
| 55 |
+
'operated by', 'managed by', 'governance', 'roadmap',
|
| 56 |
+
'build', 'create', 'maintain', 'improve', 'update'
|
| 57 |
+
],
|
| 58 |
+
'weight': 0.25
|
| 59 |
+
}
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
def analyze_prong(self, text: str, prong_name: str) -> Tuple[bool, float, List[str]]:
|
| 63 |
+
"""
|
| 64 |
+
Analyze a single Howey Test prong.
|
| 65 |
+
|
| 66 |
+
Args:
|
| 67 |
+
text: Token description/whitepaper text
|
| 68 |
+
prong_name: Name of the prong to analyze
|
| 69 |
+
|
| 70 |
+
Returns:
|
| 71 |
+
Tuple of (prong_met, confidence, evidence_keywords)
|
| 72 |
+
"""
|
| 73 |
+
if prong_name not in self.test_criteria:
|
| 74 |
+
raise ValueError(f"Invalid prong: {prong_name}")
|
| 75 |
+
|
| 76 |
+
criteria = self.test_criteria[prong_name]
|
| 77 |
+
keywords = criteria['keywords']
|
| 78 |
+
text_lower = text.lower()
|
| 79 |
+
|
| 80 |
+
# Find matching keywords
|
| 81 |
+
matches = []
|
| 82 |
+
for keyword in keywords:
|
| 83 |
+
pattern = r'\b' + re.escape(keyword) + r'\b'
|
| 84 |
+
if re.search(pattern, text_lower):
|
| 85 |
+
matches.append(keyword)
|
| 86 |
+
|
| 87 |
+
# Calculate confidence based on match density
|
| 88 |
+
match_count = len(matches)
|
| 89 |
+
word_count = len(text_lower.split())
|
| 90 |
+
match_density = (match_count / (word_count / 100)) if word_count > 0 else 0
|
| 91 |
+
|
| 92 |
+
# Prong is "met" if we have multiple keyword matches
|
| 93 |
+
prong_met = match_count >= 2
|
| 94 |
+
confidence = min(match_density / 5, 1.0) # Normalize to 0-1
|
| 95 |
+
|
| 96 |
+
return prong_met, confidence, matches
|
| 97 |
+
|
| 98 |
+
def run_howey_test(self, text: str) -> Dict:
|
| 99 |
+
"""
|
| 100 |
+
Run full Howey Test analysis on token description.
|
| 101 |
+
|
| 102 |
+
Args:
|
| 103 |
+
text: Token description/whitepaper text
|
| 104 |
+
|
| 105 |
+
Returns:
|
| 106 |
+
Dictionary with test results
|
| 107 |
+
"""
|
| 108 |
+
results = {
|
| 109 |
+
'prongs': {},
|
| 110 |
+
'prongs_met': 0,
|
| 111 |
+
'is_security': False,
|
| 112 |
+
'overall_confidence': 0.0,
|
| 113 |
+
'evidence': {},
|
| 114 |
+
'analysis_timestamp': datetime.now().isoformat()
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
# Analyze each prong
|
| 118 |
+
for prong_name in self.test_criteria.keys():
|
| 119 |
+
met, confidence, evidence = self.analyze_prong(text, prong_name)
|
| 120 |
+
|
| 121 |
+
results['prongs'][prong_name] = {
|
| 122 |
+
'met': met,
|
| 123 |
+
'confidence': confidence,
|
| 124 |
+
'evidence_count': len(evidence)
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
results['evidence'][prong_name] = evidence
|
| 128 |
+
|
| 129 |
+
if met:
|
| 130 |
+
results['prongs_met'] += 1
|
| 131 |
+
|
| 132 |
+
# Token is a security if all 4 prongs are met
|
| 133 |
+
results['is_security'] = results['prongs_met'] == 4
|
| 134 |
+
|
| 135 |
+
# Calculate overall confidence (average of prong confidences)
|
| 136 |
+
confidences = [p['confidence'] for p in results['prongs'].values()]
|
| 137 |
+
results['overall_confidence'] = sum(confidences) / len(confidences)
|
| 138 |
+
|
| 139 |
+
# Adjust confidence based on prongs met
|
| 140 |
+
if results['prongs_met'] < 4:
|
| 141 |
+
# Reduce confidence if not all prongs met
|
| 142 |
+
results['overall_confidence'] *= (results['prongs_met'] / 4)
|
| 143 |
+
|
| 144 |
+
logger.info(
|
| 145 |
+
f"Howey Test: {results['prongs_met']}/4 prongs met, "
|
| 146 |
+
f"is_security={results['is_security']}, "
|
| 147 |
+
f"confidence={results['overall_confidence']:.2f}"
|
| 148 |
+
)
|
| 149 |
+
|
| 150 |
+
return results
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
class TokenClassifier:
|
| 154 |
+
"""
|
| 155 |
+
Comprehensive token classifier using multiple frameworks.
|
| 156 |
+
- US: Howey Test
|
| 157 |
+
- EU: MiCA classification
|
| 158 |
+
- Singapore: DPT classification
|
| 159 |
+
"""
|
| 160 |
+
|
| 161 |
+
def __init__(self):
|
| 162 |
+
"""Initialize token classifier."""
|
| 163 |
+
self.howey_analyzer = HoweyTestAnalyzer()
|
| 164 |
+
logger.info("TokenClassifier initialized")
|
| 165 |
+
|
| 166 |
+
def classify_us(self, token_description: str) -> Dict:
|
| 167 |
+
"""
|
| 168 |
+
Classify token under US law (SEC Howey Test).
|
| 169 |
+
|
| 170 |
+
Args:
|
| 171 |
+
token_description: Description of token mechanics
|
| 172 |
+
|
| 173 |
+
Returns:
|
| 174 |
+
Classification result
|
| 175 |
+
"""
|
| 176 |
+
howey_result = self.howey_analyzer.run_howey_test(token_description)
|
| 177 |
+
|
| 178 |
+
classification = {
|
| 179 |
+
'jurisdiction': 'us',
|
| 180 |
+
'framework': 'SEC Howey Test',
|
| 181 |
+
'classification': 'security' if howey_result['is_security'] else 'utility',
|
| 182 |
+
'confidence': howey_result['overall_confidence'],
|
| 183 |
+
'howey_test': howey_result,
|
| 184 |
+
'regulatory_implications': self._get_us_implications(howey_result)
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
return classification
|
| 188 |
+
|
| 189 |
+
def classify_eu(self, token_description: str) -> Dict:
|
| 190 |
+
"""
|
| 191 |
+
Classify token under EU MiCA framework.
|
| 192 |
+
|
| 193 |
+
Args:
|
| 194 |
+
token_description: Token description
|
| 195 |
+
|
| 196 |
+
Returns:
|
| 197 |
+
Classification result
|
| 198 |
+
"""
|
| 199 |
+
text_lower = token_description.lower()
|
| 200 |
+
|
| 201 |
+
# MiCA categories
|
| 202 |
+
is_utility_token = any([
|
| 203 |
+
'access' in text_lower,
|
| 204 |
+
'usage' in text_lower,
|
| 205 |
+
'service' in text_lower,
|
| 206 |
+
'platform access' in text_lower
|
| 207 |
+
])
|
| 208 |
+
|
| 209 |
+
is_asset_referenced = any([
|
| 210 |
+
'backed' in text_lower,
|
| 211 |
+
'pegged' in text_lower,
|
| 212 |
+
'collateralized' in text_lower,
|
| 213 |
+
'reserve' in text_lower
|
| 214 |
+
])
|
| 215 |
+
|
| 216 |
+
is_e_money = any([
|
| 217 |
+
'fiat' in text_lower,
|
| 218 |
+
'currency' in text_lower,
|
| 219 |
+
'stablecoin' in text_lower,
|
| 220 |
+
'payment' in text_lower
|
| 221 |
+
])
|
| 222 |
+
|
| 223 |
+
# Determine primary category
|
| 224 |
+
if is_e_money:
|
| 225 |
+
category = 'e-money token'
|
| 226 |
+
elif is_asset_referenced:
|
| 227 |
+
category = 'asset-referenced token'
|
| 228 |
+
elif is_utility_token:
|
| 229 |
+
category = 'utility token'
|
| 230 |
+
else:
|
| 231 |
+
category = 'crypto-asset' # Default MiCA category
|
| 232 |
+
|
| 233 |
+
classification = {
|
| 234 |
+
'jurisdiction': 'eu',
|
| 235 |
+
'framework': 'MiCA',
|
| 236 |
+
'classification': category,
|
| 237 |
+
'confidence': 0.6, # Lower confidence for heuristic classification
|
| 238 |
+
'mica_categories': {
|
| 239 |
+
'utility_token': is_utility_token,
|
| 240 |
+
'asset_referenced_token': is_asset_referenced,
|
| 241 |
+
'e_money_token': is_e_money
|
| 242 |
+
},
|
| 243 |
+
'regulatory_implications': self._get_eu_implications(category)
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
return classification
|
| 247 |
+
|
| 248 |
+
def classify_singapore(self, token_description: str) -> Dict:
|
| 249 |
+
"""
|
| 250 |
+
Classify token under Singapore MAS framework.
|
| 251 |
+
|
| 252 |
+
Args:
|
| 253 |
+
token_description: Token description
|
| 254 |
+
|
| 255 |
+
Returns:
|
| 256 |
+
Classification result
|
| 257 |
+
"""
|
| 258 |
+
text_lower = token_description.lower()
|
| 259 |
+
|
| 260 |
+
# MAS Payment Services Act - Digital Payment Token (DPT)
|
| 261 |
+
is_dpt = any([
|
| 262 |
+
'payment' in text_lower,
|
| 263 |
+
'medium of exchange' in text_lower,
|
| 264 |
+
'store of value' in text_lower,
|
| 265 |
+
'transfer' in text_lower
|
| 266 |
+
])
|
| 267 |
+
|
| 268 |
+
is_capital_markets_product = any([
|
| 269 |
+
'security' in text_lower,
|
| 270 |
+
'investment' in text_lower,
|
| 271 |
+
'profit' in text_lower,
|
| 272 |
+
'return' in text_lower,
|
| 273 |
+
'dividend' in text_lower
|
| 274 |
+
])
|
| 275 |
+
|
| 276 |
+
# Determine category
|
| 277 |
+
if is_capital_markets_product:
|
| 278 |
+
category = 'capital markets product'
|
| 279 |
+
elif is_dpt:
|
| 280 |
+
category = 'digital payment token'
|
| 281 |
+
else:
|
| 282 |
+
category = 'unregulated token'
|
| 283 |
+
|
| 284 |
+
classification = {
|
| 285 |
+
'jurisdiction': 'singapore',
|
| 286 |
+
'framework': 'MAS PSA',
|
| 287 |
+
'classification': category,
|
| 288 |
+
'confidence': 0.6,
|
| 289 |
+
'mas_categories': {
|
| 290 |
+
'digital_payment_token': is_dpt,
|
| 291 |
+
'capital_markets_product': is_capital_markets_product
|
| 292 |
+
},
|
| 293 |
+
'regulatory_implications': self._get_singapore_implications(category)
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
return classification
|
| 297 |
+
|
| 298 |
+
def classify_all_jurisdictions(self, token_description: str) -> Dict:
|
| 299 |
+
"""
|
| 300 |
+
Classify token across all supported jurisdictions.
|
| 301 |
+
|
| 302 |
+
Args:
|
| 303 |
+
token_description: Token description/whitepaper text
|
| 304 |
+
|
| 305 |
+
Returns:
|
| 306 |
+
Dictionary of classifications per jurisdiction
|
| 307 |
+
"""
|
| 308 |
+
return {
|
| 309 |
+
'us': self.classify_us(token_description),
|
| 310 |
+
'eu': self.classify_eu(token_description),
|
| 311 |
+
'singapore': self.classify_singapore(token_description),
|
| 312 |
+
'summary': self._generate_summary(token_description)
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
def _get_us_implications(self, howey_result: Dict) -> List[str]:
|
| 316 |
+
"""Get regulatory implications for US classification."""
|
| 317 |
+
implications = []
|
| 318 |
+
|
| 319 |
+
if howey_result['is_security']:
|
| 320 |
+
implications.extend([
|
| 321 |
+
"Token is likely a security under US law",
|
| 322 |
+
"Must register with SEC or qualify for exemption",
|
| 323 |
+
"Consider Regulation D (private placement) or Regulation A+",
|
| 324 |
+
"Must comply with securities laws for trading",
|
| 325 |
+
"May need broker-dealer registration for exchanges"
|
| 326 |
+
])
|
| 327 |
+
else:
|
| 328 |
+
implications.extend([
|
| 329 |
+
"Token may be a utility token (not a security)",
|
| 330 |
+
"Still subject to FinCEN MSB registration if used for payments",
|
| 331 |
+
"State money transmitter licenses may be required",
|
| 332 |
+
"Consumer protection laws still apply",
|
| 333 |
+
"Monitor SEC guidance - classification can change"
|
| 334 |
+
])
|
| 335 |
+
|
| 336 |
+
return implications
|
| 337 |
+
|
| 338 |
+
def _get_eu_implications(self, category: str) -> List[str]:
|
| 339 |
+
"""Get regulatory implications for EU classification."""
|
| 340 |
+
implications_map = {
|
| 341 |
+
'e-money token': [
|
| 342 |
+
"Subject to strict MiCA e-money token requirements",
|
| 343 |
+
"Need authorization as e-money institution",
|
| 344 |
+
"Must maintain 1:1 backing with fiat reserves",
|
| 345 |
+
"Enhanced consumer protection requirements",
|
| 346 |
+
"Effective from June 2024"
|
| 347 |
+
],
|
| 348 |
+
'asset-referenced token': [
|
| 349 |
+
"Subject to MiCA asset-referenced token regime",
|
| 350 |
+
"Must maintain reserve of referenced assets",
|
| 351 |
+
"Requires authorization from regulator",
|
| 352 |
+
"Ongoing reporting and transparency requirements",
|
| 353 |
+
"White paper must be approved"
|
| 354 |
+
],
|
| 355 |
+
'utility token': [
|
| 356 |
+
"Lower regulatory burden under MiCA",
|
| 357 |
+
"Still requires white paper publication",
|
| 358 |
+
"Consumer protection rules apply",
|
| 359 |
+
"Marketing restrictions apply",
|
| 360 |
+
"Effective from July 2024"
|
| 361 |
+
],
|
| 362 |
+
'crypto-asset': [
|
| 363 |
+
"General MiCA crypto-asset rules apply",
|
| 364 |
+
"CASP authorization needed for services",
|
| 365 |
+
"White paper required for public offerings",
|
| 366 |
+
"AML/CTF compliance mandatory"
|
| 367 |
+
]
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
return implications_map.get(category, ["MiCA framework applies"])
|
| 371 |
+
|
| 372 |
+
def _get_singapore_implications(self, category: str) -> List[str]:
|
| 373 |
+
"""Get regulatory implications for Singapore classification."""
|
| 374 |
+
implications_map = {
|
| 375 |
+
'digital payment token': [
|
| 376 |
+
"Requires DPT service provider license from MAS",
|
| 377 |
+
"Must comply with Payment Services Act",
|
| 378 |
+
"AML/CFT requirements apply",
|
| 379 |
+
"Technology risk management guidelines",
|
| 380 |
+
"Fit and proper criteria for operators"
|
| 381 |
+
],
|
| 382 |
+
'capital markets product': [
|
| 383 |
+
"Subject to Securities and Futures Act",
|
| 384 |
+
"Requires CMS license from MAS",
|
| 385 |
+
"Prospectus or exemption required",
|
| 386 |
+
"Ongoing reporting obligations",
|
| 387 |
+
"Higher regulatory scrutiny"
|
| 388 |
+
],
|
| 389 |
+
'unregulated token': [
|
| 390 |
+
"May not require MAS licensing",
|
| 391 |
+
"Still subject to general laws",
|
| 392 |
+
"Monitor for regulatory changes",
|
| 393 |
+
"Consumer protection laws apply"
|
| 394 |
+
]
|
| 395 |
+
}
|
| 396 |
+
|
| 397 |
+
return implications_map.get(category, ["Review MAS guidelines"])
|
| 398 |
+
|
| 399 |
+
def _generate_summary(self, token_description: str) -> Dict:
|
| 400 |
+
"""Generate summary across jurisdictions."""
|
| 401 |
+
us_result = self.classify_us(token_description)
|
| 402 |
+
eu_result = self.classify_eu(token_description)
|
| 403 |
+
sg_result = self.classify_singapore(token_description)
|
| 404 |
+
|
| 405 |
+
is_security_anywhere = (
|
| 406 |
+
us_result['classification'] == 'security' or
|
| 407 |
+
sg_result['classification'] == 'capital markets product'
|
| 408 |
+
)
|
| 409 |
+
|
| 410 |
+
return {
|
| 411 |
+
'is_security_anywhere': is_security_anywhere,
|
| 412 |
+
'most_restrictive_jurisdiction': 'us' if us_result['classification'] == 'security' else 'eu',
|
| 413 |
+
'classifications': {
|
| 414 |
+
'us': us_result['classification'],
|
| 415 |
+
'eu': eu_result['classification'],
|
| 416 |
+
'singapore': sg_result['classification']
|
| 417 |
+
},
|
| 418 |
+
'recommendation': (
|
| 419 |
+
"Consult securities lawyer immediately - token appears to be a security"
|
| 420 |
+
if is_security_anywhere else
|
| 421 |
+
"Token may qualify as utility token, but verify with legal counsel"
|
| 422 |
+
)
|
| 423 |
+
}
|
| 424 |
+
|
| 425 |
+
|
| 426 |
+
# Convenience function
|
| 427 |
+
def classify_token(token_description: str, jurisdiction: Optional[str] = None) -> Dict:
|
| 428 |
+
"""
|
| 429 |
+
Quick classify a token.
|
| 430 |
+
|
| 431 |
+
Args:
|
| 432 |
+
token_description: Token description/whitepaper
|
| 433 |
+
jurisdiction: Specific jurisdiction ('us', 'eu', 'singapore') or None for all
|
| 434 |
+
|
| 435 |
+
Returns:
|
| 436 |
+
Classification result
|
| 437 |
+
"""
|
| 438 |
+
classifier = TokenClassifier()
|
| 439 |
+
|
| 440 |
+
if jurisdiction:
|
| 441 |
+
if jurisdiction == 'us':
|
| 442 |
+
return classifier.classify_us(token_description)
|
| 443 |
+
elif jurisdiction == 'eu':
|
| 444 |
+
return classifier.classify_eu(token_description)
|
| 445 |
+
elif jurisdiction == 'singapore':
|
| 446 |
+
return classifier.classify_singapore(token_description)
|
| 447 |
+
else:
|
| 448 |
+
raise ValueError(f"Unsupported jurisdiction: {jurisdiction}")
|
| 449 |
+
else:
|
| 450 |
+
return classifier.classify_all_jurisdictions(token_description)
|
| 451 |
+
|
| 452 |
+
|
| 453 |
+
if __name__ == "__main__":
|
| 454 |
+
# Example usage
|
| 455 |
+
sample_token = """
|
| 456 |
+
Our governance token allows holders to vote on protocol upgrades and earn
|
| 457 |
+
rewards from transaction fees. Tokens are sold in a public sale at $0.50 each.
|
| 458 |
+
The development team will use funds to build the platform and market the product.
|
| 459 |
+
Early investors expect significant returns as the platform grows and token value
|
| 460 |
+
appreciates. The team manages the treasury and executes the roadmap.
|
| 461 |
+
"""
|
| 462 |
+
|
| 463 |
+
print("\n=== Token Classification ===\n")
|
| 464 |
+
|
| 465 |
+
# Full analysis
|
| 466 |
+
results = classify_token(sample_token)
|
| 467 |
+
|
| 468 |
+
print("US Classification:")
|
| 469 |
+
us = results['us']
|
| 470 |
+
print(f" Classification: {us['classification']}")
|
| 471 |
+
print(f" Confidence: {us['confidence']:.2f}")
|
| 472 |
+
print(f" Howey Test: {us['howey_test']['prongs_met']}/4 prongs met")
|
| 473 |
+
|
| 474 |
+
print("\nEU Classification:")
|
| 475 |
+
eu = results['eu']
|
| 476 |
+
print(f" Classification: {eu['classification']}")
|
| 477 |
+
print(f" Confidence: {eu['confidence']:.2f}")
|
| 478 |
+
|
| 479 |
+
print("\nSingapore Classification:")
|
| 480 |
+
sg = results['singapore']
|
| 481 |
+
print(f" Classification: {sg['classification']}")
|
| 482 |
+
print(f" Confidence: {sg['confidence']:.2f}")
|
| 483 |
+
|
| 484 |
+
print("\nSummary:")
|
| 485 |
+
summary = results['summary']
|
| 486 |
+
print(f" Is security anywhere: {summary['is_security_anywhere']}")
|
| 487 |
+
print(f" Recommendation: {summary['recommendation']}")
|
src/config.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Configuration module for the Crypto Compliance Intelligence Agent.
|
| 3 |
+
Loads environment variables and provides centralized configuration.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
from dotenv import load_dotenv
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
|
| 10 |
+
# Load environment variables from .env file
|
| 11 |
+
load_dotenv()
|
| 12 |
+
|
| 13 |
+
class Config:
|
| 14 |
+
"""
|
| 15 |
+
Central configuration class for the application.
|
| 16 |
+
All configuration parameters are loaded from environment variables.
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
# API Keys
|
| 20 |
+
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
|
| 21 |
+
HF_TOKEN = os.getenv("HF_TOKEN")
|
| 22 |
+
GITHUB_TOKEN = os.getenv("GITHUB_TOKEN")
|
| 23 |
+
|
| 24 |
+
# Model Configuration
|
| 25 |
+
EMBEDDING_MODEL = os.getenv("EMBEDDING_MODEL", "sentence-transformers/all-MiniLM-L6-v2")
|
| 26 |
+
FINBERT_MODEL = os.getenv("FINBERT_MODEL", "ProsusAI/finbert")
|
| 27 |
+
LEGAL_BERT_MODEL = os.getenv("LEGAL_BERT_MODEL", "nlpaueb/legal-bert-base-uncased")
|
| 28 |
+
|
| 29 |
+
# ChromaDB Configuration
|
| 30 |
+
CHROMA_PERSIST_DIR = os.getenv("CHROMA_PERSIST_DIR", "./data/chroma_db")
|
| 31 |
+
COLLECTION_NAME = os.getenv("COLLECTION_NAME", "regulations_kb")
|
| 32 |
+
|
| 33 |
+
# Gemini Configuration
|
| 34 |
+
GEMINI_MODEL = os.getenv("GEMINI_MODEL", "gemini-2.0-flash-exp")
|
| 35 |
+
GEMINI_TEMPERATURE = float(os.getenv("GEMINI_TEMPERATURE", "0.1"))
|
| 36 |
+
GEMINI_MAX_TOKENS = int(os.getenv("GEMINI_MAX_TOKENS", "8192"))
|
| 37 |
+
|
| 38 |
+
# Application Settings
|
| 39 |
+
PROJECT_ROOT = Path(__file__).parent.parent
|
| 40 |
+
DATA_DIR = PROJECT_ROOT / "data"
|
| 41 |
+
REGULATIONS_DIR = DATA_DIR / "regulations"
|
| 42 |
+
|
| 43 |
+
# Jurisdictions supported
|
| 44 |
+
SUPPORTED_JURISDICTIONS = ["us", "eu", "singapore", "uk", "uae"]
|
| 45 |
+
|
| 46 |
+
# Logging Configuration
|
| 47 |
+
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO")
|
| 48 |
+
LOG_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
| 49 |
+
|
| 50 |
+
@classmethod
|
| 51 |
+
def validate(cls):
|
| 52 |
+
"""
|
| 53 |
+
Validate that all required configuration is present.
|
| 54 |
+
Raises ValueError if critical configuration is missing.
|
| 55 |
+
"""
|
| 56 |
+
if not cls.GEMINI_API_KEY:
|
| 57 |
+
raise ValueError("GEMINI_API_KEY is required but not set in environment variables")
|
| 58 |
+
|
| 59 |
+
# Ensure required directories exist
|
| 60 |
+
cls.DATA_DIR.mkdir(exist_ok=True, parents=True)
|
| 61 |
+
cls.REGULATIONS_DIR.mkdir(exist_ok=True, parents=True)
|
| 62 |
+
|
| 63 |
+
for jurisdiction in cls.SUPPORTED_JURISDICTIONS:
|
| 64 |
+
jurisdiction_dir = cls.REGULATIONS_DIR / jurisdiction
|
| 65 |
+
jurisdiction_dir.mkdir(exist_ok=True, parents=True)
|
| 66 |
+
|
| 67 |
+
return True
|
| 68 |
+
|
| 69 |
+
# Validate configuration on import
|
| 70 |
+
Config.validate()
|
src/data/__init__.py
ADDED
|
File without changes
|
src/data/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (196 Bytes). View file
|
|
|
src/data/__pycache__/vectordb.cpython-313.pyc
ADDED
|
Binary file (14.7 kB). View file
|
|
|
src/data/regulations/eu/mica-asset-referenced-tokens-2024.json
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"id": "eu-mica-asset-referenced-tokens-2024",
|
| 3 |
+
"jurisdiction": "eu",
|
| 4 |
+
"agency": "ESMA (European Securities and Markets Authority) / MiCA Framework",
|
| 5 |
+
"title": "MiCA Regulation - Asset-Referenced Tokens (ARTs) for Real Estate",
|
| 6 |
+
"summary": "EU Markets in Crypto-Assets (MiCA) regulation requirements for asset-referenced tokens backed by real estate or other assets. Covers authorization requirements, reserve management, white paper publication, and ongoing supervision for ARTs. Effective June 30, 2024 with transitional period through December 2024.",
|
| 7 |
+
"status": "effective",
|
| 8 |
+
"announced_date": "2023-05-31",
|
| 9 |
+
"effective_date": "2024-06-30",
|
| 10 |
+
"last_updated": "2024-09-01",
|
| 11 |
+
"source_url": "https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:32023R1114",
|
| 12 |
+
"full_text_url": "https://eur-lex.europa.eu/legal-content/EN/TXT/PDF/?uri=CELEX:32023R1114",
|
| 13 |
+
"crypto_activities_affected": [
|
| 14 |
+
"tokenization",
|
| 15 |
+
"asset-referenced-tokens",
|
| 16 |
+
"issuance",
|
| 17 |
+
"custody",
|
| 18 |
+
"secondary-markets"
|
| 19 |
+
],
|
| 20 |
+
"tags": [
|
| 21 |
+
"mica",
|
| 22 |
+
"asset-referenced-tokens",
|
| 23 |
+
"real-estate",
|
| 24 |
+
"authorization",
|
| 25 |
+
"white-paper",
|
| 26 |
+
"reserve-requirements",
|
| 27 |
+
"esma"
|
| 28 |
+
],
|
| 29 |
+
"requirements": [
|
| 30 |
+
{
|
| 31 |
+
"requirement": "ART Issuer Authorization from National Competent Authority",
|
| 32 |
+
"description": "Must obtain authorization as ART issuer from national competent authority (NCA) in home member state (e.g., BaFin in Germany, AMF in France). Application requires: business plan, governance arrangements, risk management framework, IT systems description, recovery plan. Authorization process: 6-12 months. Application fee varies by country (€10K-50K). Issuers established in third countries must appoint legal representative in EU.",
|
| 33 |
+
"mandatory": true,
|
| 34 |
+
"deadline_days": 0,
|
| 35 |
+
"estimated_cost_usd": {
|
| 36 |
+
"min": 75000,
|
| 37 |
+
"max": 150000,
|
| 38 |
+
"currency": "EUR",
|
| 39 |
+
"notes": "NCA application fee (€10K-50K) + legal preparation (€50K-100K) + consultancy fees"
|
| 40 |
+
},
|
| 41 |
+
"severity": "critical",
|
| 42 |
+
"exemptions": ["Credit institutions and electronic money institutions already authorized under existing EU framework"]
|
| 43 |
+
},
|
| 44 |
+
{
|
| 45 |
+
"requirement": "Minimum Own Funds Requirement",
|
| 46 |
+
"description": "ART issuers must maintain own funds equal to greater of: (1) €350,000, OR (2) 2% of average amount of reserve assets, OR (3) 25% of fixed overheads of preceding year. Own funds must be held in cash or highly liquid financial instruments. For real estate-backed ARTs exceeding €100M circulation, requirement may increase to 3% of reserves. Annual audit required.",
|
| 47 |
+
"mandatory": true,
|
| 48 |
+
"deadline_days": 0,
|
| 49 |
+
"estimated_cost_usd": {
|
| 50 |
+
"min": 350000,
|
| 51 |
+
"max": 2000000,
|
| 52 |
+
"currency": "EUR",
|
| 53 |
+
"notes": "Minimum €350K but scales with reserve size - real estate ARTs typically €500K-2M"
|
| 54 |
+
},
|
| 55 |
+
"severity": "critical",
|
| 56 |
+
"exemptions": []
|
| 57 |
+
},
|
| 58 |
+
{
|
| 59 |
+
"requirement": "Crypto-Asset White Paper Publication and Notification",
|
| 60 |
+
"description": "Must prepare and publish white paper containing: (1) issuer information and governance, (2) detailed description of real estate assets backing tokens, (3) rights and obligations of token holders, (4) stabilization mechanism, (5) reserve management policy, (6) comprehensive risk warnings (minimum 15 categories), (7) environmental/climate impact. White paper must be notified to NCA 20 business days before publication. NCA may prohibit or suspend offering. White paper valid for 12 months. Filing fee: €5K-20K per jurisdiction.",
|
| 61 |
+
"mandatory": true,
|
| 62 |
+
"deadline_days": 60,
|
| 63 |
+
"estimated_cost_usd": {
|
| 64 |
+
"min": 40000,
|
| 65 |
+
"max": 100000,
|
| 66 |
+
"currency": "EUR",
|
| 67 |
+
"notes": "Legal drafting (€30K-70K) + NCA notification fees (€5K-20K) + translations for multi-country offerings"
|
| 68 |
+
},
|
| 69 |
+
"severity": "critical",
|
| 70 |
+
"exemptions": ["Offerings to qualified investors only (but must still file white paper with NCA)"]
|
| 71 |
+
},
|
| 72 |
+
{
|
| 73 |
+
"requirement": "Reserve of Assets Management and Segregation",
|
| 74 |
+
"description": "Must maintain reserve of assets with value equal to at least 100% of circulation value of ARTs. Reserve composition: (1) at least 30% in cash deposits at credit institutions, (2) remainder in highly liquid financial instruments with minimal credit risk. For real estate ARTs: property valuations must be updated quarterly by independent appraiser. Reserve assets must be legally segregated and held by authorized custodian. Daily reconciliation and monthly reports to NCA required. Reserve cannot be pledged or encumbered.",
|
| 75 |
+
"mandatory": true,
|
| 76 |
+
"deadline_days": 90,
|
| 77 |
+
"estimated_cost_usd": {
|
| 78 |
+
"min": 60000,
|
| 79 |
+
"max": 150000,
|
| 80 |
+
"currency": "EUR",
|
| 81 |
+
"notes": "Custodian setup + quarterly property valuations (€15K-40K each) + compliance systems + legal segregation structure"
|
| 82 |
+
},
|
| 83 |
+
"severity": "critical",
|
| 84 |
+
"exemptions": []
|
| 85 |
+
},
|
| 86 |
+
{
|
| 87 |
+
"requirement": "Redemption Rights and Mechanisms",
|
| 88 |
+
"description": "Token holders must have right to redeem ARTs for reserve assets or fiat equivalent at any time, at least once per month. Redemption price based on fair market value of underlying real estate (minus reasonable fees up to 5%). Must maintain liquid reserves (30% of total) to meet redemption requests. If redemptions exceed 25% of reserves in 7-day period, issuer may suspend redemptions for up to 3 months with NCA approval. Smart contract must enforce redemption rights automatically.",
|
| 89 |
+
"mandatory": true,
|
| 90 |
+
"deadline_days": 90,
|
| 91 |
+
"estimated_cost_usd": {
|
| 92 |
+
"min": 25000,
|
| 93 |
+
"max": 60000,
|
| 94 |
+
"currency": "EUR",
|
| 95 |
+
"notes": "Smart contract development for redemption mechanism + liquidity management systems"
|
| 96 |
+
},
|
| 97 |
+
"severity": "high",
|
| 98 |
+
"exemptions": []
|
| 99 |
+
},
|
| 100 |
+
{
|
| 101 |
+
"requirement": "Conflicts of Interest and Governance Requirements",
|
| 102 |
+
"description": "Must establish management body with at least 2 members (4 if ART circulation exceeds €100M). Independent audit committee required. Conflict of interest policy must address: related party transactions, property valuations, reserve management. AML/CFT compliance officer required (separate from management). Internal audit function required for issuers exceeding €50M circulation. Fit and proper assessments for all board members.",
|
| 103 |
+
"mandatory": true,
|
| 104 |
+
"deadline_days": 120,
|
| 105 |
+
"estimated_cost_usd": {
|
| 106 |
+
"min": 80000,
|
| 107 |
+
"max": 200000,
|
| 108 |
+
"currency": "EUR",
|
| 109 |
+
"notes": "First year: board member recruitment + compliance officer salary (€60K-120K) + governance setup + fit-and-proper assessments"
|
| 110 |
+
},
|
| 111 |
+
"severity": "high",
|
| 112 |
+
"exemptions": []
|
| 113 |
+
},
|
| 114 |
+
{
|
| 115 |
+
"requirement": "Marketing Communications and Advertising Rules",
|
| 116 |
+
"description": "All marketing must: (1) be clearly identifiable as such, (2) be fair, clear, not misleading, (3) include risk warnings, (4) state ART issuer name and authorization status, (5) reference white paper. Prohibited claims: capital protection, guaranteed returns, comparison to securities unless fair. Marketing via social media influencers must disclose commercial relationship. NCA may require pre-approval of marketing materials. Non-compliant ads subject to €5M fines or 3% of annual turnover.",
|
| 117 |
+
"mandatory": true,
|
| 118 |
+
"deadline_days": 45,
|
| 119 |
+
"estimated_cost_usd": {
|
| 120 |
+
"min": 20000,
|
| 121 |
+
"max": 50000,
|
| 122 |
+
"currency": "EUR",
|
| 123 |
+
"notes": "Legal review of all marketing materials + compliance procedures + risk warning templates"
|
| 124 |
+
},
|
| 125 |
+
"severity": "medium",
|
| 126 |
+
"exemptions": []
|
| 127 |
+
},
|
| 128 |
+
{
|
| 129 |
+
"requirement": "Orderly Wind-Down Plan and Recovery Arrangements",
|
| 130 |
+
"description": "Must maintain detailed plan for orderly wind-down in case of insolvency or license revocation. Plan must include: token redemption process, reserve asset liquidation timeline (realistic for real estate: 12-24 months), communication to token holders, appointment of liquidator. Recovery plan required for ARTs exceeding €100M circulation, including stress testing scenarios (property value decline, mass redemptions, custody failure). Annual plan updates required.",
|
| 131 |
+
"mandatory": true,
|
| 132 |
+
"deadline_days": 180,
|
| 133 |
+
"estimated_cost_usd": {
|
| 134 |
+
"min": 30000,
|
| 135 |
+
"max": 80000,
|
| 136 |
+
"currency": "EUR",
|
| 137 |
+
"notes": "Legal and financial advisors for wind-down and recovery planning + stress testing"
|
| 138 |
+
},
|
| 139 |
+
"severity": "high",
|
| 140 |
+
"exemptions": []
|
| 141 |
+
},
|
| 142 |
+
{
|
| 143 |
+
"requirement": "Reporting and Transparency Obligations",
|
| 144 |
+
"description": "Ongoing reporting requirements: (1) quarterly reports to NCA (reserve composition, token circulation, redemptions), (2) annual audited financial statements, (3) publish quarterly reserve attestation on website, (4) immediate notification of material events (property damage, valuation changes >10%, technical failures, security breaches). Token holder reporting: quarterly updates on property performance and valuations. Public disclosure of any changes to white paper within 24 hours.",
|
| 145 |
+
"mandatory": true,
|
| 146 |
+
"deadline_days": 0,
|
| 147 |
+
"estimated_cost_usd": {
|
| 148 |
+
"min": 40000,
|
| 149 |
+
"max": 100000,
|
| 150 |
+
"currency": "EUR",
|
| 151 |
+
"notes": "Annual ongoing: external audit (€20K-50K) + quarterly reporting + compliance monitoring + disclosure systems"
|
| 152 |
+
},
|
| 153 |
+
"severity": "medium",
|
| 154 |
+
"exemptions": []
|
| 155 |
+
},
|
| 156 |
+
{
|
| 157 |
+
"requirement": "Cybersecurity and Operational Resilience (DORA Compliance)",
|
| 158 |
+
"description": "Must comply with Digital Operational Resilience Act (DORA) requirements: (1) ICT risk management framework, (2) incident reporting (within 4 hours for major incidents), (3) digital operational resilience testing (including penetration tests), (4) third-party ICT risk management, (5) threat-led penetration testing (TLPT) for significant ARTs. DORA compliance deadline: January 17, 2025. Non-compliance penalties up to ���10M or 2% of global turnover.",
|
| 159 |
+
"mandatory": true,
|
| 160 |
+
"deadline_days": 365,
|
| 161 |
+
"estimated_cost_usd": {
|
| 162 |
+
"min": 75000,
|
| 163 |
+
"max": 200000,
|
| 164 |
+
"currency": "EUR",
|
| 165 |
+
"notes": "DORA compliance program + penetration testing + incident response systems + third-party risk management"
|
| 166 |
+
},
|
| 167 |
+
"severity": "high",
|
| 168 |
+
"exemptions": []
|
| 169 |
+
},
|
| 170 |
+
{
|
| 171 |
+
"requirement": "AML/CFT Compliance Under 6AMLD",
|
| 172 |
+
"description": "Must comply with EU's 6th Anti-Money Laundering Directive (6AMLD) and Transfer of Funds Regulation (TFR). Requirements: (1) customer due diligence (CDD) for all token holders, (2) enhanced due diligence (EDD) for high-risk customers (PEPs, high-value transactions >€1000), (3) transaction monitoring and suspicious activity reporting (SAR) to FIU, (4) Travel Rule compliance for transfers >€1000, (5) sanctions screening (EU, OFAC, UN), (6) record retention 5 years. AML officer and independent audit required.",
|
| 173 |
+
"mandatory": true,
|
| 174 |
+
"deadline_days": 120,
|
| 175 |
+
"estimated_cost_usd": {
|
| 176 |
+
"min": 60000,
|
| 177 |
+
"max": 150000,
|
| 178 |
+
"currency": "EUR",
|
| 179 |
+
"notes": "First year: AML/KYC system (€30K-70K) + compliance officer + training + transaction monitoring tools"
|
| 180 |
+
},
|
| 181 |
+
"severity": "critical",
|
| 182 |
+
"exemptions": []
|
| 183 |
+
}
|
| 184 |
+
],
|
| 185 |
+
"penalties": [
|
| 186 |
+
{
|
| 187 |
+
"violation": "Operating Without ART Issuer Authorization",
|
| 188 |
+
"penalty_type": "Administrative Fines + Criminal Prosecution",
|
| 189 |
+
"amount_usd": {
|
| 190 |
+
"min": 500000,
|
| 191 |
+
"max": 5000000,
|
| 192 |
+
"notes": "Administrative fines up to €5M or 3% of total annual turnover + criminal prosecution in some member states (imprisonment up to 5 years) + disgorgement of profits"
|
| 193 |
+
},
|
| 194 |
+
"additional_consequences": [
|
| 195 |
+
"Immediate cease-and-desist order",
|
| 196 |
+
"Token holder redemption rights at issuance price",
|
| 197 |
+
"Criminal prosecution (varies by member state)",
|
| 198 |
+
"Permanent ban from crypto-asset activities in EU",
|
| 199 |
+
"Asset seizure and freezing orders"
|
| 200 |
+
]
|
| 201 |
+
},
|
| 202 |
+
{
|
| 203 |
+
"violation": "Inadequate Reserve Management or Commingling",
|
| 204 |
+
"penalty_type": "Administrative Fines + License Revocation",
|
| 205 |
+
"amount_usd": {
|
| 206 |
+
"min": 250000,
|
| 207 |
+
"max": 2500000,
|
| 208 |
+
"notes": "Fines up to €2.5M or 2% of annual turnover + license revocation + mandatory wind-down + civil liability to token holders"
|
| 209 |
+
},
|
| 210 |
+
"additional_consequences": [
|
| 211 |
+
"Authorization revocation and mandatory wind-down",
|
| 212 |
+
"Civil liability for token holder losses",
|
| 213 |
+
"Enhanced supervision and restrictions",
|
| 214 |
+
"NCA-appointed special administrator"
|
| 215 |
+
]
|
| 216 |
+
},
|
| 217 |
+
{
|
| 218 |
+
"violation": "False or Misleading White Paper Information",
|
| 219 |
+
"penalty_type": "Administrative Fines + Civil Liability",
|
| 220 |
+
"amount_usd": {
|
| 221 |
+
"min": 100000,
|
| 222 |
+
"max": 1000000,
|
| 223 |
+
"notes": "Fines up to €1M or 1% of annual turnover + civil liability to investors who relied on false information + potential criminal fraud charges"
|
| 224 |
+
},
|
| 225 |
+
"additional_consequences": [
|
| 226 |
+
"Suspension of ART offerings",
|
| 227 |
+
"Civil lawsuits from token holders (rescission + damages)",
|
| 228 |
+
"Mandatory white paper corrections and re-publication",
|
| 229 |
+
"Reputational damage and loss of authorization"
|
| 230 |
+
]
|
| 231 |
+
},
|
| 232 |
+
{
|
| 233 |
+
"violation": "Failure to Comply with AML/CFT Requirements",
|
| 234 |
+
"penalty_type": "Administrative Fines + Criminal Penalties",
|
| 235 |
+
"amount_usd": {
|
| 236 |
+
"min": 500000,
|
| 237 |
+
"max": 5000000,
|
| 238 |
+
"notes": "AML fines up to €5M or 10% of annual turnover (whichever higher) + criminal prosecution for money laundering facilitation + license revocation"
|
| 239 |
+
},
|
| 240 |
+
"additional_consequences": [
|
| 241 |
+
"Criminal prosecution for money laundering (imprisonment up to 10 years)",
|
| 242 |
+
"Immediate license suspension or revocation",
|
| 243 |
+
"Financial intelligence unit (FIU) investigation",
|
| 244 |
+
"Permanent industry ban",
|
| 245 |
+
"Sanctions screening violations may trigger additional EU/UN penalties"
|
| 246 |
+
]
|
| 247 |
+
}
|
| 248 |
+
],
|
| 249 |
+
"regulatory_guidance": [
|
| 250 |
+
"MiCA distinguishes ARTs from e-money tokens (EMTs) - real estate tokens typically classified as ARTs",
|
| 251 |
+
"If ART is also deemed financial instrument under MiFID II, additional requirements may apply",
|
| 252 |
+
"Issuers may choose home member state for authorization (jurisdiction shopping permitted)",
|
| 253 |
+
"Once authorized in one EU member state, passporting rights allow operations across entire EU/EEA",
|
| 254 |
+
"Real estate ARTs face challenges with reserve management due to property illiquidity - NCAs may require higher liquid reserves (40-50%)",
|
| 255 |
+
"ESMA developing regulatory technical standards (RTS) for reserve management - expected Q1 2025",
|
| 256 |
+
"Transitional period until December 30, 2024 for existing issuers to achieve compliance"
|
| 257 |
+
],
|
| 258 |
+
"related_regulations": [
|
| 259 |
+
"eu-mica-casp-requirements-2024",
|
| 260 |
+
"eu-dora-operational-resilience-2025",
|
| 261 |
+
"eu-6amld-aml-2024",
|
| 262 |
+
"eu-tfr-travel-rule-2024"
|
| 263 |
+
],
|
| 264 |
+
"confidence": 0.92,
|
| 265 |
+
"notes": "MiCA's ART framework presents significant challenges for real estate tokenization due to illiquid nature of property assets versus required liquid reserves and monthly redemption rights. Total first-year compliance costs for real estate ART: €800K-2M. Ongoing annual costs: €300K-600K. Many real estate tokenization projects may instead pursue MiFID II 'financial instrument' classification or limit offerings to qualified investors to reduce compliance burden."
|
| 266 |
+
}
|
src/data/regulations/schema.json
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
| 3 |
+
"title": "Regulatory Rule Schema",
|
| 4 |
+
"description": "Schema for crypto regulatory rules across jurisdictions",
|
| 5 |
+
"type": "object",
|
| 6 |
+
"required": ["id", "title", "jurisdiction", "agency", "status", "summary"],
|
| 7 |
+
"properties": {
|
| 8 |
+
"id": {
|
| 9 |
+
"type": "string",
|
| 10 |
+
"description": "Unique identifier: {jurisdiction}-{year}-{short-name}",
|
| 11 |
+
"pattern": "^(us|eu|singapore|uk|uae)-[0-9]{4}-[a-z0-9-]+$",
|
| 12 |
+
"examples": ["us-2024-custody", "eu-2024-mica-implementation"]
|
| 13 |
+
},
|
| 14 |
+
"title": {
|
| 15 |
+
"type": "string",
|
| 16 |
+
"description": "Official title of the regulation",
|
| 17 |
+
"examples": ["Crypto Custody Rule", "Markets in Crypto-Assets Regulation"]
|
| 18 |
+
},
|
| 19 |
+
"jurisdiction": {
|
| 20 |
+
"type": "string",
|
| 21 |
+
"enum": ["us", "eu", "singapore", "uk", "uae"],
|
| 22 |
+
"description": "Jurisdiction where regulation applies"
|
| 23 |
+
},
|
| 24 |
+
"agency": {
|
| 25 |
+
"type": "string",
|
| 26 |
+
"description": "Regulatory agency issuing the rule",
|
| 27 |
+
"examples": ["SEC", "MiCA", "MAS", "FCA", "VARA"]
|
| 28 |
+
},
|
| 29 |
+
"status": {
|
| 30 |
+
"type": "string",
|
| 31 |
+
"enum": ["proposed", "effective", "repealed", "under_review"],
|
| 32 |
+
"description": "Current status of the regulation"
|
| 33 |
+
},
|
| 34 |
+
"announced_date": {
|
| 35 |
+
"type": "string",
|
| 36 |
+
"format": "date",
|
| 37 |
+
"description": "Date regulation was announced (ISO 8601)"
|
| 38 |
+
},
|
| 39 |
+
"comment_deadline": {
|
| 40 |
+
"type": "string",
|
| 41 |
+
"format": "date",
|
| 42 |
+
"description": "Deadline for public comments (if applicable)"
|
| 43 |
+
},
|
| 44 |
+
"effective_date": {
|
| 45 |
+
"type": "string",
|
| 46 |
+
"description": "When regulation becomes effective (ISO date or 'Q1 2025')"
|
| 47 |
+
},
|
| 48 |
+
"summary": {
|
| 49 |
+
"type": "string",
|
| 50 |
+
"description": "Brief summary of the regulation (2-3 sentences)"
|
| 51 |
+
},
|
| 52 |
+
"full_text_url": {
|
| 53 |
+
"type": "string",
|
| 54 |
+
"format": "uri",
|
| 55 |
+
"description": "URL to official regulation text"
|
| 56 |
+
},
|
| 57 |
+
"crypto_activities_affected": {
|
| 58 |
+
"type": "array",
|
| 59 |
+
"items": {
|
| 60 |
+
"type": "string",
|
| 61 |
+
"enum": [
|
| 62 |
+
"exchange",
|
| 63 |
+
"custody",
|
| 64 |
+
"staking",
|
| 65 |
+
"lending",
|
| 66 |
+
"borrowing",
|
| 67 |
+
"nft_marketplace",
|
| 68 |
+
"defi_protocol",
|
| 69 |
+
"payment_processing",
|
| 70 |
+
"token_issuance",
|
| 71 |
+
"mining",
|
| 72 |
+
"wallet_services",
|
| 73 |
+
"otc_trading",
|
| 74 |
+
"derivatives",
|
| 75 |
+
"dao"
|
| 76 |
+
]
|
| 77 |
+
},
|
| 78 |
+
"description": "List of crypto activities this regulation affects"
|
| 79 |
+
},
|
| 80 |
+
"requirements": {
|
| 81 |
+
"type": "array",
|
| 82 |
+
"items": {
|
| 83 |
+
"type": "object",
|
| 84 |
+
"properties": {
|
| 85 |
+
"requirement": {
|
| 86 |
+
"type": "string",
|
| 87 |
+
"description": "Specific requirement text"
|
| 88 |
+
},
|
| 89 |
+
"applies_to": {
|
| 90 |
+
"type": "array",
|
| 91 |
+
"items": { "type": "string" },
|
| 92 |
+
"description": "Which activities this requirement applies to"
|
| 93 |
+
},
|
| 94 |
+
"deadline": {
|
| 95 |
+
"type": "string",
|
| 96 |
+
"description": "Deadline for compliance"
|
| 97 |
+
}
|
| 98 |
+
}
|
| 99 |
+
},
|
| 100 |
+
"description": "Specific compliance requirements"
|
| 101 |
+
},
|
| 102 |
+
"penalties": {
|
| 103 |
+
"type": "object",
|
| 104 |
+
"properties": {
|
| 105 |
+
"fines_min": {
|
| 106 |
+
"type": "number",
|
| 107 |
+
"description": "Minimum fine amount (USD)"
|
| 108 |
+
},
|
| 109 |
+
"fines_max": {
|
| 110 |
+
"type": "number",
|
| 111 |
+
"description": "Maximum fine amount (USD)"
|
| 112 |
+
},
|
| 113 |
+
"other_penalties": {
|
| 114 |
+
"type": "array",
|
| 115 |
+
"items": { "type": "string" },
|
| 116 |
+
"description": "Non-monetary penalties (e.g., 'Business shutdown', 'Criminal charges')"
|
| 117 |
+
}
|
| 118 |
+
},
|
| 119 |
+
"description": "Penalties for non-compliance"
|
| 120 |
+
},
|
| 121 |
+
"exemptions": {
|
| 122 |
+
"type": "array",
|
| 123 |
+
"items": {
|
| 124 |
+
"type": "object",
|
| 125 |
+
"properties": {
|
| 126 |
+
"exemption_type": { "type": "string" },
|
| 127 |
+
"description": { "type": "string" },
|
| 128 |
+
"eligibility_criteria": {
|
| 129 |
+
"type": "array",
|
| 130 |
+
"items": { "type": "string" }
|
| 131 |
+
}
|
| 132 |
+
}
|
| 133 |
+
},
|
| 134 |
+
"description": "Available exemptions or safe harbors"
|
| 135 |
+
},
|
| 136 |
+
"confidence": {
|
| 137 |
+
"type": "number",
|
| 138 |
+
"minimum": 0.0,
|
| 139 |
+
"maximum": 1.0,
|
| 140 |
+
"description": "Confidence score for this regulation data (0.0-1.0)"
|
| 141 |
+
},
|
| 142 |
+
"source": {
|
| 143 |
+
"type": "object",
|
| 144 |
+
"properties": {
|
| 145 |
+
"type": {
|
| 146 |
+
"type": "string",
|
| 147 |
+
"enum": ["official", "news", "analysis", "scraped"],
|
| 148 |
+
"description": "Source type"
|
| 149 |
+
},
|
| 150 |
+
"url": {
|
| 151 |
+
"type": "string",
|
| 152 |
+
"format": "uri"
|
| 153 |
+
},
|
| 154 |
+
"retrieved_date": {
|
| 155 |
+
"type": "string",
|
| 156 |
+
"format": "date-time"
|
| 157 |
+
}
|
| 158 |
+
},
|
| 159 |
+
"description": "Data source information"
|
| 160 |
+
},
|
| 161 |
+
"related_regulations": {
|
| 162 |
+
"type": "array",
|
| 163 |
+
"items": {
|
| 164 |
+
"type": "string",
|
| 165 |
+
"description": "IDs of related regulations"
|
| 166 |
+
},
|
| 167 |
+
"description": "References to related regulatory rules"
|
| 168 |
+
},
|
| 169 |
+
"tags": {
|
| 170 |
+
"type": "array",
|
| 171 |
+
"items": {
|
| 172 |
+
"type": "string"
|
| 173 |
+
},
|
| 174 |
+
"description": "Keywords/tags for categorization",
|
| 175 |
+
"examples": [["kyc", "aml", "reporting"], ["securities", "howey-test"]]
|
| 176 |
+
}
|
| 177 |
+
}
|
| 178 |
+
}
|
src/data/regulations/singapore/mas-cmp-real-estate-tokens-2024.json
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"id": "singapore-mas-cmp-real-estate-tokens-2024",
|
| 3 |
+
"jurisdiction": "singapore",
|
| 4 |
+
"agency": "MAS (Monetary Authority of Singapore)",
|
| 5 |
+
"title": "Capital Markets Products Framework for Real Estate Security Tokens",
|
| 6 |
+
"summary": "MAS regulatory framework for digital tokens representing interests in real estate under Securities and Futures Act (SFA). Covers licensing requirements for offers, custody, and secondary trading of real estate security tokens. Includes guidelines on prospectus requirements, exemptions for sophisticated investors, and DPT service licensing.",
|
| 7 |
+
"status": "effective",
|
| 8 |
+
"announced_date": "2020-01-28",
|
| 9 |
+
"effective_date": "2020-01-28",
|
| 10 |
+
"last_updated": "2024-08-30",
|
| 11 |
+
"source_url": "https://www.mas.gov.sg/regulation/capital-markets/digital-tokens",
|
| 12 |
+
"full_text_url": "https://www.mas.gov.sg/-/media/mas/regulations-and-financial-stability/regulations-guidance-and-licensing/securities-futures-and-fund-management/guidelines/a-guide-to-digital-token-offerings.pdf",
|
| 13 |
+
"crypto_activities_affected": [
|
| 14 |
+
"tokenization",
|
| 15 |
+
"securities-offering",
|
| 16 |
+
"custody",
|
| 17 |
+
"secondary-trading",
|
| 18 |
+
"payment-services"
|
| 19 |
+
],
|
| 20 |
+
"tags": [
|
| 21 |
+
"capital-markets-products",
|
| 22 |
+
"security-tokens",
|
| 23 |
+
"real-estate",
|
| 24 |
+
"sfa",
|
| 25 |
+
"prospectus",
|
| 26 |
+
"cms-license",
|
| 27 |
+
"dpt-services"
|
| 28 |
+
],
|
| 29 |
+
"requirements": [
|
| 30 |
+
{
|
| 31 |
+
"requirement": "Capital Markets Product (CMP) Classification and Legal Opinion",
|
| 32 |
+
"description": "Real estate tokens representing ownership, profit-sharing, or derivative interests are regulated as 'capital markets products' (CMP) under SFA. Must obtain legal opinion from Singapore law firm confirming: (1) token is CMP (typically 'units in collective investment scheme' or 'debentures'), (2) appropriate exemptions apply, (3) compliance roadmap. Legal opinion required for MAS submissions. Cost: SGD $30K-60K ($22K-45K USD).",
|
| 33 |
+
"mandatory": true,
|
| 34 |
+
"deadline_days": 0,
|
| 35 |
+
"estimated_cost_usd": {
|
| 36 |
+
"min": 22000,
|
| 37 |
+
"max": 45000,
|
| 38 |
+
"currency": "USD",
|
| 39 |
+
"notes": "Singapore securities law firm legal opinion on CMP classification"
|
| 40 |
+
},
|
| 41 |
+
"severity": "critical",
|
| 42 |
+
"exemptions": ["Tokens that are pure payment tokens or utility tokens (not investment products)"]
|
| 43 |
+
},
|
| 44 |
+
{
|
| 45 |
+
"requirement": "Capital Markets Services (CMS) License for Issuance Activities",
|
| 46 |
+
"description": "Entities making offers of real estate security tokens must hold CMS license for 'dealing in capital markets products' OR rely on prospectus exemptions. CMS license application requires: (1) SGD $250K base capital + SGD $150K liquid assets, (2) fit and proper directors/shareholders, (3) business plan, (4) compliance arrangements, (5) office in Singapore. Application process: 6-9 months. Application fee: SGD $10K. Annual license fee: SGD $8K-15K based on activity.",
|
| 47 |
+
"mandatory": true,
|
| 48 |
+
"deadline_days": 0,
|
| 49 |
+
"estimated_cost_usd": {
|
| 50 |
+
"min": 300000,
|
| 51 |
+
"max": 400000,
|
| 52 |
+
"currency": "USD",
|
| 53 |
+
"notes": "SGD $250K base capital + SGD $150K liquid capital + SGD $10K application fee + legal/consultancy (SGD $50K-100K)"
|
| 54 |
+
},
|
| 55 |
+
"severity": "critical",
|
| 56 |
+
"exemptions": [
|
| 57 |
+
"Private placement to institutional/accredited investors only (up to 50 persons in 12 months)",
|
| 58 |
+
"Offers via licensed intermediary who holds CMS license"
|
| 59 |
+
]
|
| 60 |
+
},
|
| 61 |
+
{
|
| 62 |
+
"requirement": "Prospectus Registration or Exemption Reliance",
|
| 63 |
+
"description": "Public offers of real estate security tokens require prospectus registered with MAS unless exempt. Prospectus must include: property details and valuations, financial forecasts, risk factors (minimum 20 factors), legal structure, use of proceeds, management bios, audited financials. Registration process: 3-6 months. Most issuers rely on exemptions: (1) offers to institutional/accredited investors only, (2) private placement exemption (up to 50 persons), (3) small offers exemption (up to SGD $5M in 12 months). Exemption reliance requires filing Form 45 with MAS.",
|
| 64 |
+
"mandatory": true,
|
| 65 |
+
"deadline_days": 14,
|
| 66 |
+
"estimated_cost_usd": {
|
| 67 |
+
"min": 8000,
|
| 68 |
+
"max": 80000,
|
| 69 |
+
"currency": "USD",
|
| 70 |
+
"notes": "If full prospectus: SGD $80K-120K. If exemption: Form 45 filing + legal (SGD $10K-15K)"
|
| 71 |
+
},
|
| 72 |
+
"severity": "critical",
|
| 73 |
+
"exemptions": [
|
| 74 |
+
"Offers to institutional investors only",
|
| 75 |
+
"Offers to accredited investors (individual net assets >SGD $2M OR income >SGD $300K)",
|
| 76 |
+
"Private placement (≤50 persons in 12 months)"
|
| 77 |
+
]
|
| 78 |
+
},
|
| 79 |
+
{
|
| 80 |
+
"requirement": "Digital Payment Token (DPT) Service License (if applicable)",
|
| 81 |
+
"description": "If real estate tokens can be used for payment or exchange (not purely investment), may require DPT service license under Payment Services Act. Activities requiring DPT license: (1) operating exchange for DPT trading, (2) facilitating DPT transfers, (3) providing DPT custody wallet services. License requires: SGD $250K base capital (higher for exchange/custody: up to SGD $1.5M), fit and proper, AML/CFT compliance, technology risk management. Application: 6-12 months. Most real estate tokens qualify for exemption as they are purely securities, not payment instruments.",
|
| 82 |
+
"mandatory": false,
|
| 83 |
+
"deadline_days": 90,
|
| 84 |
+
"estimated_cost_usd": {
|
| 85 |
+
"min": 200000,
|
| 86 |
+
"max": 1200000,
|
| 87 |
+
"currency": "USD",
|
| 88 |
+
"notes": "If DPT license required: SGD $250K-1.5M capital + application and compliance costs (SGD $50K-100K)"
|
| 89 |
+
},
|
| 90 |
+
"severity": "high",
|
| 91 |
+
"exemptions": ["Security tokens used solely for investment (not payment) are exempt from PSA"]
|
| 92 |
+
},
|
| 93 |
+
{
|
| 94 |
+
"requirement": "Technology Risk Management (TRM) Guidelines Compliance",
|
| 95 |
+
"description": "Must comply with MAS Technology Risk Management Guidelines including: (1) cybersecurity controls and testing, (2) system availability targets (>99.5% for critical systems), (3) data security and encryption, (4) incident management and reporting (notify MAS within 1 hour for severe incidents), (5) business continuity plan (RTO <4 hours), (6) change management procedures, (7) third-party vendor risk management. Annual independent audit required. TRM non-compliance can result in license suspension.",
|
| 96 |
+
"mandatory": true,
|
| 97 |
+
"deadline_days": 180,
|
| 98 |
+
"estimated_cost_usd": {
|
| 99 |
+
"min": 40000,
|
| 100 |
+
"max": 100000,
|
| 101 |
+
"currency": "USD",
|
| 102 |
+
"notes": "First year: cybersecurity assessment + penetration testing + incident response + BCP development. Annual: SGD $30K-60K"
|
| 103 |
+
},
|
| 104 |
+
"severity": "high",
|
| 105 |
+
"exemptions": []
|
| 106 |
+
},
|
| 107 |
+
{
|
| 108 |
+
"requirement": "Anti-Money Laundering and Countering Financing of Terrorism (AML/CFT)",
|
| 109 |
+
"description": "Must comply with MAS Notice SFA04-N02 (AML/CFT for Capital Markets) including: (1) customer due diligence (CDD) - verify identity, source of funds, (2) enhanced due diligence (EDD) for high-risk customers (PEPs, countries on FATF list, transactions >SGD $20K), (3) ongoing monitoring and transaction screening, (4) suspicious transaction reporting (STR) to STRO within 15 days, (5) record keeping (6 years), (6) AML/CFT officer appointment, (7) regular staff training (minimum annually). Independent audit every 2 years.",
|
| 110 |
+
"mandatory": true,
|
| 111 |
+
"deadline_days": 90,
|
| 112 |
+
"estimated_cost_usd": {
|
| 113 |
+
"min": 35000,
|
| 114 |
+
"max": 85000,
|
| 115 |
+
"currency": "USD",
|
| 116 |
+
"notes": "First year: AML/CFT system setup (SGD $25K-50K) + compliance officer + training + screening tools. Annual ongoing: SGD $20K-40K"
|
| 117 |
+
},
|
| 118 |
+
"severity": "critical",
|
| 119 |
+
"exemptions": []
|
| 120 |
+
},
|
| 121 |
+
{
|
| 122 |
+
"requirement": "Custody and Safekeeping Arrangements (if providing custody)",
|
| 123 |
+
"description": "If issuer provides custody of tokens (holding private keys on behalf of investors), must either: (1) obtain CMS license for 'providing custodian services for securities', OR (2) appoint licensed custodian. Licensed custody requires: SGD $1M base capital + SGD $500K liquid capital, segregation of client assets, insurance coverage (minimum SGD $1M or 5% of AUM), annual audit, cybersecurity controls. Alternative: use third-party licensed custodian (fees 0.1-0.5% of AUM annually).",
|
| 124 |
+
"mandatory": true,
|
| 125 |
+
"deadline_days": 120,
|
| 126 |
+
"estimated_cost_usd": {
|
| 127 |
+
"min": 50000,
|
| 128 |
+
"max": 1200000,
|
| 129 |
+
"currency": "USD",
|
| 130 |
+
"notes": "If self-custody: SGD $1.5M capital + compliance infrastructure. If third-party: integration + fees (0.1-0.5% AUM)"
|
| 131 |
+
},
|
| 132 |
+
"severity": "high",
|
| 133 |
+
"exemptions": ["If investors hold their own private keys (non-custodial model) - but must provide clear disclosures"]
|
| 134 |
+
},
|
| 135 |
+
{
|
| 136 |
+
"requirement": "Approved Exchange or Recognized Market Operator (if secondary trading)",
|
| 137 |
+
"description": "Secondary trading of real estate security tokens must occur on: (1) Approved Exchange (AE) licensed by MAS, OR (2) Recognized Market Operator (RMO), OR (3) exempt organized market. Operating unlicensed exchange is criminal offense. AE/RMO license requires: SGD $5M base capital, technology infrastructure, market surveillance, clearing arrangements, business rules approved by MAS. Application: 12-24 months. Most issuers restrict secondary trading or partner with licensed exchanges like iSTOX, Fundnel.",
|
| 138 |
+
"mandatory": true,
|
| 139 |
+
"deadline_days": 0,
|
| 140 |
+
"estimated_cost_usd": {
|
| 141 |
+
"min": 30000,
|
| 142 |
+
"max": 5000000,
|
| 143 |
+
"currency": "USD",
|
| 144 |
+
"notes": "If operate own exchange: SGD $5M+ capital + infrastructure. If use third-party: integration fees (SGD $30K-100K) + listing fees"
|
| 145 |
+
},
|
| 146 |
+
"severity": "high",
|
| 147 |
+
"exemptions": ["Transfers restricted to original purchasers/affiliates", "Over-the-counter bilateral transfers (must still comply with securities laws)"]
|
| 148 |
+
},
|
| 149 |
+
{
|
| 150 |
+
"requirement": "Advertising and Marketing Guidelines",
|
| 151 |
+
"description": "All marketing materials must comply with MAS FAA Notice FAA-N03 (Advertising Guidelines). Requirements: (1) fair, balanced, not misleading, (2) risk warnings prominently displayed, (3) past performance disclaimers, (4) avoid unsubstantiated claims or guarantees, (5) specify target investor type (retail/accredited/institutional), (6) include license number and regulatory status. Marketing to retail investors requires additional disclosures and may require prospectus. Social media posts and influencer marketing subject to same rules. Non-compliant ads can result in SGD $50K-250K fines.",
|
| 152 |
+
"mandatory": true,
|
| 153 |
+
"deadline_days": 30,
|
| 154 |
+
"estimated_cost_usd": {
|
| 155 |
+
"min": 15000,
|
| 156 |
+
"max": 40000,
|
| 157 |
+
"currency": "USD",
|
| 158 |
+
"notes": "Legal review of all marketing materials + compliance procedures + staff training"
|
| 159 |
+
},
|
| 160 |
+
"severity": "medium",
|
| 161 |
+
"exemptions": []
|
| 162 |
+
},
|
| 163 |
+
{
|
| 164 |
+
"requirement": "Continuous Disclosure and Ongoing Reporting",
|
| 165 |
+
"description": "Ongoing obligations after offering: (1) material event disclosure within 24 hours (property damage, valuation changes >15%, management changes, breaches), (2) semi-annual unaudited financial statements, (3) annual audited financial statements within 5 months of FY-end, (4) annual property valuations by independent valuer, (5) quarterly updates to token holders on property performance. If token holders exceed 50, must appoint share registrar. Records must be kept for 5 years. Failure to disclose material events can trigger civil liability.",
|
| 166 |
+
"mandatory": true,
|
| 167 |
+
"deadline_days": 0,
|
| 168 |
+
"estimated_cost_usd": {
|
| 169 |
+
"min": 30000,
|
| 170 |
+
"max": 80000,
|
| 171 |
+
"currency": "USD",
|
| 172 |
+
"notes": "Annual ongoing: audit (SGD $20K-50K) + property valuation (SGD $10K-30K) + reporting systems + compliance staff"
|
| 173 |
+
},
|
| 174 |
+
"severity": "medium",
|
| 175 |
+
"exemptions": []
|
| 176 |
+
},
|
| 177 |
+
{
|
| 178 |
+
"requirement": "Product Due Diligence and Suitability Assessment",
|
| 179 |
+
"description": "If offering to non-institutional investors, must conduct: (1) product due diligence to assess risks and suitability, (2) customer knowledge assessment (KYA) to understand investor profile, (3) suitability assessment matching product to customer, (4) enhanced warnings for complex products or retail investors, (5) cooling-off period (7 days for retail investors). Must document all assessments. Mis-selling can result in investor restitution orders + MAS penalties (up to SGD $2M).",
|
| 180 |
+
"mandatory": true,
|
| 181 |
+
"deadline_days": 0,
|
| 182 |
+
"estimated_cost_usd": {
|
| 183 |
+
"min": 20000,
|
| 184 |
+
"max": 50000,
|
| 185 |
+
"currency": "USD",
|
| 186 |
+
"notes": "Suitability assessment system development + compliance procedures + staff training"
|
| 187 |
+
},
|
| 188 |
+
"severity": "high",
|
| 189 |
+
"exemptions": ["Offers to institutional and accredited investors only (exempt from suitability requirements)"]
|
| 190 |
+
}
|
| 191 |
+
],
|
| 192 |
+
"penalties": [
|
| 193 |
+
{
|
| 194 |
+
"violation": "Unlicensed Capital Markets Services (Unauthorized Dealing)",
|
| 195 |
+
"penalty_type": "Criminal Prosecution + Civil Penalties",
|
| 196 |
+
"amount_usd": {
|
| 197 |
+
"min": 75000,
|
| 198 |
+
"max": 150000,
|
| 199 |
+
"notes": "Criminal fine up to SGD $150K OR imprisonment up to 3 years + civil penalties up to SGD $2M + disgorgement of profits + investor restitution"
|
| 200 |
+
},
|
| 201 |
+
"additional_consequences": [
|
| 202 |
+
"Criminal conviction and imprisonment (up to 3 years)",
|
| 203 |
+
"Civil penalty orders up to SGD $2M",
|
| 204 |
+
"Director disqualification orders (up to 5 years)",
|
| 205 |
+
"Investor rescission rights (full refund + interest)",
|
| 206 |
+
"Permanent ban from financial services industry in Singapore"
|
| 207 |
+
]
|
| 208 |
+
},
|
| 209 |
+
{
|
| 210 |
+
"violation": "False or Misleading Prospectus/Offering Document",
|
| 211 |
+
"penalty_type": "Criminal Prosecution + Civil Liability",
|
| 212 |
+
"amount_usd": {
|
| 213 |
+
"min": 112500,
|
| 214 |
+
"max": 225000,
|
| 215 |
+
"notes": "Criminal fine up to SGD $150K OR imprisonment up to 2 years + civil compensation to investors + MAS enforcement action"
|
| 216 |
+
},
|
| 217 |
+
"additional_consequences": [
|
| 218 |
+
"Criminal prosecution (up to 2 years imprisonment)",
|
| 219 |
+
"Civil liability to all investors who relied on document",
|
| 220 |
+
"MAS prohibition orders preventing future fundraising",
|
| 221 |
+
"Director personal liability for losses",
|
| 222 |
+
"Reputational damage and business closure"
|
| 223 |
+
]
|
| 224 |
+
},
|
| 225 |
+
{
|
| 226 |
+
"violation": "AML/CFT Breaches or Inadequate Controls",
|
| 227 |
+
"penalty_type": "Civil Penalties + License Revocation",
|
| 228 |
+
"amount_usd": {
|
| 229 |
+
"min": 75000,
|
| 230 |
+
"max": 750000,
|
| 231 |
+
"notes": "Civil penalties up to SGD $1M + license suspension or revocation + criminal prosecution for willful breaches (up to SGD $500K fine + 10 years imprisonment)"
|
| 232 |
+
},
|
| 233 |
+
"additional_consequences": [
|
| 234 |
+
"License suspension (30-90 days) or revocation",
|
| 235 |
+
"Criminal prosecution for willful AML violations",
|
| 236 |
+
"Enhanced supervision and remediation orders",
|
| 237 |
+
"Mandatory independent compliance audit at issuer's expense",
|
| 238 |
+
"Reputational damage and loss of banking relationships"
|
| 239 |
+
]
|
| 240 |
+
},
|
| 241 |
+
{
|
| 242 |
+
"violation": "Operating Unlicensed Exchange or Market",
|
| 243 |
+
"penalty_type": "Criminal Prosecution + Shutdown Order",
|
| 244 |
+
"amount_usd": {
|
| 245 |
+
"min": 37500,
|
| 246 |
+
"max": 112500,
|
| 247 |
+
"notes": "Criminal fine up to SGD $150K OR imprisonment up to 2 years + immediate shutdown order + disgorgement of trading fees + investor restitution"
|
| 248 |
+
},
|
| 249 |
+
"additional_consequences": [
|
| 250 |
+
"Criminal conviction and imprisonment (up to 2 years)",
|
| 251 |
+
"Immediate cease-and-desist and platform shutdown",
|
| 252 |
+
"Asset freezing and seizure orders",
|
| 253 |
+
"Disgorgement of all trading fees and profits",
|
| 254 |
+
"Permanent industry ban"
|
| 255 |
+
]
|
| 256 |
+
}
|
| 257 |
+
],
|
| 258 |
+
"regulatory_guidance": [
|
| 259 |
+
"MAS 'Guide to Digital Token Offerings' (2020) is primary guidance for token classification",
|
| 260 |
+
"Real estate tokens typically classified as 'units in collective investment scheme' or 'debentures' under SFA",
|
| 261 |
+
"Most issuers rely on private placement exemption (≤50 investors) or accredited investor exemption to avoid prospectus",
|
| 262 |
+
"MAS takes substance-over-form approach - classification based on economic reality, not label",
|
| 263 |
+
"Secondary trading restrictions common - many issuers prohibit transfers or limit to accredited investors",
|
| 264 |
+
"Singapore has several licensed digital securities platforms: iSTOX, Fundnel, ADDX - recommended to partner rather than build own",
|
| 265 |
+
"MAS Project Guardian exploring use cases for tokenized assets including real estate - potential regulatory sandbox participation",
|
| 266 |
+
"Singapore-based issuers targeting global investors must comply with both SFA and foreign securities laws"
|
| 267 |
+
],
|
| 268 |
+
"related_regulations": [
|
| 269 |
+
"singapore-mas-psa-dpt-2024",
|
| 270 |
+
"singapore-mas-cms-custody-2024",
|
| 271 |
+
"singapore-mas-trm-guidelines-2024"
|
| 272 |
+
],
|
| 273 |
+
"confidence": 0.94,
|
| 274 |
+
"notes": "Singapore's regulatory framework for real estate security tokens is well-developed and clear. Total first-year costs for compliant real estate token offering: SGD $500K-1.5M (USD $375K-1.1M) depending on licensing path. Private placements to accredited investors offer most cost-effective route (SGD $100K-200K compliance costs). Retail offerings require full prospectus and CMS license (SGD $800K-1.5M). Singapore's Project Guardian and supportive regulatory approach make it attractive jurisdiction for tokenization pilots."
|
| 275 |
+
}
|
src/data/regulations/uae/vara-sto-real-estate-2024.json
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"id": "uae-vara-sto-real-estate-2024",
|
| 3 |
+
"jurisdiction": "uae",
|
| 4 |
+
"agency": "VARA (Virtual Asset Regulatory Authority)",
|
| 5 |
+
"title": "Security Token Offering (STO) Requirements for Real Estate Tokenization",
|
| 6 |
+
"summary": "VARA regulations for tokenizing real estate assets through security token offerings in Dubai. Covers licensing, capital requirements, property valuation, and investor protection measures for fractionalized real estate ownership.",
|
| 7 |
+
"status": "effective",
|
| 8 |
+
"announced_date": "2023-06-15",
|
| 9 |
+
"effective_date": "2023-10-01",
|
| 10 |
+
"last_updated": "2024-08-20",
|
| 11 |
+
"source_url": "https://www.vara.ae/en/regulations/sto-framework",
|
| 12 |
+
"full_text_url": "https://www.vara.ae/media/regulations/sto-guidance.pdf",
|
| 13 |
+
"crypto_activities_affected": [
|
| 14 |
+
"tokenization",
|
| 15 |
+
"custody",
|
| 16 |
+
"exchange",
|
| 17 |
+
"advisory"
|
| 18 |
+
],
|
| 19 |
+
"tags": [
|
| 20 |
+
"security-tokens",
|
| 21 |
+
"real-estate",
|
| 22 |
+
"sto",
|
| 23 |
+
"licensing",
|
| 24 |
+
"capital-requirements",
|
| 25 |
+
"property-valuation"
|
| 26 |
+
],
|
| 27 |
+
"requirements": [
|
| 28 |
+
{
|
| 29 |
+
"requirement": "VARA STO License Application",
|
| 30 |
+
"description": "Entities offering security tokens backed by real estate must obtain a VARA Security Token Offering (STO) License, not the standard Virtual Asset License. Application process takes 12-18 months and requires detailed business plan, technical infrastructure assessment, and compliance framework.",
|
| 31 |
+
"mandatory": true,
|
| 32 |
+
"deadline_days": 0,
|
| 33 |
+
"estimated_cost_usd": {
|
| 34 |
+
"min": 50000,
|
| 35 |
+
"max": 75000,
|
| 36 |
+
"currency": "USD",
|
| 37 |
+
"notes": "License application fee: AED 50,000 + legal preparation AED 100,000-200,000"
|
| 38 |
+
},
|
| 39 |
+
"severity": "critical",
|
| 40 |
+
"exemptions": []
|
| 41 |
+
},
|
| 42 |
+
{
|
| 43 |
+
"requirement": "Minimum Capital Requirement",
|
| 44 |
+
"description": "AED 2,000,000 (approximately USD $545,000) minimum paid-up capital required for STO license holders. Capital must be held in UAE-based bank and cannot be used for operational expenses during first 12 months.",
|
| 45 |
+
"mandatory": true,
|
| 46 |
+
"deadline_days": 0,
|
| 47 |
+
"estimated_cost_usd": {
|
| 48 |
+
"min": 545000,
|
| 49 |
+
"max": 545000,
|
| 50 |
+
"currency": "USD",
|
| 51 |
+
"notes": "AED 2M regulatory capital requirement"
|
| 52 |
+
},
|
| 53 |
+
"severity": "critical",
|
| 54 |
+
"exemptions": []
|
| 55 |
+
},
|
| 56 |
+
{
|
| 57 |
+
"requirement": "Independent Property Valuation",
|
| 58 |
+
"description": "All real estate assets must be independently valued by VARA-approved property valuers within 90 days of tokenization. Revaluation required annually. Valuation report must include: fair market value, rental income analysis, comparable sales, and tokenization suitability assessment.",
|
| 59 |
+
"mandatory": true,
|
| 60 |
+
"deadline_days": 90,
|
| 61 |
+
"estimated_cost_usd": {
|
| 62 |
+
"min": 25000,
|
| 63 |
+
"max": 75000,
|
| 64 |
+
"currency": "USD",
|
| 65 |
+
"notes": "Per property valuation: AED 25K-75K depending on property value. Annual revaluation: AED 15K-40K"
|
| 66 |
+
},
|
| 67 |
+
"severity": "high",
|
| 68 |
+
"exemptions": []
|
| 69 |
+
},
|
| 70 |
+
{
|
| 71 |
+
"requirement": "Token Structure Documentation",
|
| 72 |
+
"description": "Detailed token economics documentation required: rights attached to tokens (voting, dividend, redemption), smart contract audit by VARA-approved auditor, token supply management, and lock-up periods for founding team and insiders.",
|
| 73 |
+
"mandatory": true,
|
| 74 |
+
"deadline_days": 60,
|
| 75 |
+
"estimated_cost_usd": {
|
| 76 |
+
"min": 40000,
|
| 77 |
+
"max": 100000,
|
| 78 |
+
"currency": "USD",
|
| 79 |
+
"notes": "Smart contract audit: AED 50K-150K + legal documentation: AED 75K-200K"
|
| 80 |
+
},
|
| 81 |
+
"severity": "high",
|
| 82 |
+
"exemptions": []
|
| 83 |
+
},
|
| 84 |
+
{
|
| 85 |
+
"requirement": "Investor Protection Framework",
|
| 86 |
+
"description": "Implement investor protection measures including: minimum investment thresholds (AED 50,000 for retail, AED 500,000 for qualified investors), suitability assessments, risk disclosure documents (minimum 25 pages), cooling-off period (14 days), and investor complaint resolution mechanism.",
|
| 87 |
+
"mandatory": true,
|
| 88 |
+
"deadline_days": 90,
|
| 89 |
+
"estimated_cost_usd": {
|
| 90 |
+
"min": 30000,
|
| 91 |
+
"max": 60000,
|
| 92 |
+
"currency": "USD",
|
| 93 |
+
"notes": "Documentation preparation + compliance systems setup"
|
| 94 |
+
},
|
| 95 |
+
"severity": "high",
|
| 96 |
+
"exemptions": []
|
| 97 |
+
},
|
| 98 |
+
{
|
| 99 |
+
"requirement": "AML/CTF Compliance Program",
|
| 100 |
+
"description": "Comprehensive Anti-Money Laundering and Counter-Terrorist Financing program aligned with FATF recommendations. Must include: KYC procedures, transaction monitoring system, suspicious activity reporting (SAR), record retention (7 years), and annual independent audit.",
|
| 101 |
+
"mandatory": true,
|
| 102 |
+
"deadline_days": 120,
|
| 103 |
+
"estimated_cost_usd": {
|
| 104 |
+
"min": 75000,
|
| 105 |
+
"max": 150000,
|
| 106 |
+
"currency": "USD",
|
| 107 |
+
"notes": "First year: system setup + compliance officer + training. Annual ongoing: AED 100K-200K"
|
| 108 |
+
},
|
| 109 |
+
"severity": "critical",
|
| 110 |
+
"exemptions": []
|
| 111 |
+
},
|
| 112 |
+
{
|
| 113 |
+
"requirement": "Custody and Escrow Arrangements",
|
| 114 |
+
"description": "Property title deeds must be held in escrow by UAE-licensed escrow agent. Digital tokens must be custodied by VARA-licensed custodian with insurance coverage (minimum 125% of token value). Multi-signature wallet arrangements required with at least 3-of-5 key holders.",
|
| 115 |
+
"mandatory": true,
|
| 116 |
+
"deadline_days": 60,
|
| 117 |
+
"estimated_cost_usd": {
|
| 118 |
+
"min": 50000,
|
| 119 |
+
"max": 100000,
|
| 120 |
+
"currency": "USD",
|
| 121 |
+
"notes": "Escrow setup + custody fees (0.5-1% annually of AUM) + insurance premiums"
|
| 122 |
+
},
|
| 123 |
+
"severity": "high",
|
| 124 |
+
"exemptions": []
|
| 125 |
+
},
|
| 126 |
+
{
|
| 127 |
+
"requirement": "Marketing and Disclosure Requirements",
|
| 128 |
+
"description": "All marketing materials must be pre-approved by VARA (15-day review period). Required disclosures: property location and details, token economics, historical performance, fees and charges, liquidity risks, property management details, and regulatory status. False or misleading statements prohibited (penalties up to AED 10M).",
|
| 129 |
+
"mandatory": true,
|
| 130 |
+
"deadline_days": 45,
|
| 131 |
+
"estimated_cost_usd": {
|
| 132 |
+
"min": 20000,
|
| 133 |
+
"max": 40000,
|
| 134 |
+
"currency": "USD",
|
| 135 |
+
"notes": "Legal review + marketing compliance + VARA review fees"
|
| 136 |
+
},
|
| 137 |
+
"severity": "medium",
|
| 138 |
+
"exemptions": []
|
| 139 |
+
},
|
| 140 |
+
{
|
| 141 |
+
"requirement": "Ongoing Reporting Obligations",
|
| 142 |
+
"description": "Quarterly financial reports, semi-annual property performance updates, annual audited financial statements, token holder registry updates (within 5 business days of transfers), and material event notifications (within 24 hours). All reports submitted via VARA's digital portal.",
|
| 143 |
+
"mandatory": true,
|
| 144 |
+
"deadline_days": 0,
|
| 145 |
+
"estimated_cost_usd": {
|
| 146 |
+
"min": 40000,
|
| 147 |
+
"max": 80000,
|
| 148 |
+
"currency": "USD",
|
| 149 |
+
"notes": "Annual ongoing: compliance staff + audit fees + reporting systems"
|
| 150 |
+
},
|
| 151 |
+
"severity": "medium",
|
| 152 |
+
"exemptions": []
|
| 153 |
+
},
|
| 154 |
+
{
|
| 155 |
+
"requirement": "Technology and Cybersecurity Standards",
|
| 156 |
+
"description": "ISO 27001 certification required within 12 months of license grant. Systems must have: penetration testing (quarterly), disaster recovery plan (RTO < 4 hours), encryption of all customer data, and incident response procedures. Annual external cybersecurity audit mandatory.",
|
| 157 |
+
"mandatory": true,
|
| 158 |
+
"deadline_days": 365,
|
| 159 |
+
"estimated_cost_usd": {
|
| 160 |
+
"min": 60000,
|
| 161 |
+
"max": 120000,
|
| 162 |
+
"currency": "USD",
|
| 163 |
+
"notes": "ISO certification + penetration testing + ongoing security measures"
|
| 164 |
+
},
|
| 165 |
+
"severity": "high",
|
| 166 |
+
"exemptions": []
|
| 167 |
+
}
|
| 168 |
+
],
|
| 169 |
+
"penalties": [
|
| 170 |
+
{
|
| 171 |
+
"violation": "Operating without STO License",
|
| 172 |
+
"penalty_type": "Administrative fine + Criminal prosecution",
|
| 173 |
+
"amount_usd": {
|
| 174 |
+
"min": 270000,
|
| 175 |
+
"max": 2700000,
|
| 176 |
+
"notes": "AED 1M-10M fine + possible imprisonment (up to 10 years) + asset seizure"
|
| 177 |
+
},
|
| 178 |
+
"additional_consequences": [
|
| 179 |
+
"Permanent ban from UAE virtual asset sector",
|
| 180 |
+
"Seizure of all tokenized assets",
|
| 181 |
+
"Investor restitution orders",
|
| 182 |
+
"Public disclosure of violation"
|
| 183 |
+
]
|
| 184 |
+
},
|
| 185 |
+
{
|
| 186 |
+
"violation": "Inadequate AML/CTF controls",
|
| 187 |
+
"penalty_type": "Administrative fine + Remediation order",
|
| 188 |
+
"amount_usd": {
|
| 189 |
+
"min": 135000,
|
| 190 |
+
"max": 1350000,
|
| 191 |
+
"notes": "AED 500K-5M depending on severity + mandatory compliance officer replacement"
|
| 192 |
+
},
|
| 193 |
+
"additional_consequences": [
|
| 194 |
+
"Enhanced supervision (12-24 months)",
|
| 195 |
+
"License suspension (30-90 days)",
|
| 196 |
+
"Mandatory independent compliance review"
|
| 197 |
+
]
|
| 198 |
+
},
|
| 199 |
+
{
|
| 200 |
+
"violation": "False or misleading marketing",
|
| 201 |
+
"penalty_type": "Administrative fine + Corrective disclosure",
|
| 202 |
+
"amount_usd": {
|
| 203 |
+
"min": 54000,
|
| 204 |
+
"max": 540000,
|
| 205 |
+
"notes": "AED 200K-2M + mandatory corrective advertising at issuer's expense"
|
| 206 |
+
},
|
| 207 |
+
"additional_consequences": [
|
| 208 |
+
"Marketing pre-approval required for 24 months",
|
| 209 |
+
"Investor compensation for losses",
|
| 210 |
+
"Public censure"
|
| 211 |
+
]
|
| 212 |
+
}
|
| 213 |
+
],
|
| 214 |
+
"regulatory_guidance": [
|
| 215 |
+
"VARA considers real estate tokenization as securities offering, not virtual asset trading",
|
| 216 |
+
"Tokens representing fractional property ownership are 'Capital Market Products' under UAE law",
|
| 217 |
+
"Cross-border offerings require additional approvals from UAE Securities and Commodities Authority (SCA)",
|
| 218 |
+
"Property ownership transfer must comply with Dubai Land Department (DLD) requirements",
|
| 219 |
+
"Rental income distribution to token holders subject to UAE corporate tax (9% from June 2023)"
|
| 220 |
+
],
|
| 221 |
+
"related_regulations": [
|
| 222 |
+
"uae-vara-custody-2023",
|
| 223 |
+
"uae-sca-securities-2020",
|
| 224 |
+
"uae-dld-property-tokenization-2024",
|
| 225 |
+
"uae-aml-ctf-2022"
|
| 226 |
+
],
|
| 227 |
+
"confidence": 0.95,
|
| 228 |
+
"notes": "Real estate tokenization in UAE requires coordination between VARA (digital asset regulation), SCA (securities regulation), and DLD (property registration). Most stringent requirements apply to retail investor offerings. Qualified investor offerings may have reduced requirements."
|
| 229 |
+
}
|
src/data/regulations/uk/fca-cis-property-tokens-2024.json
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"id": "uk-fca-cis-property-tokens-2024",
|
| 3 |
+
"jurisdiction": "uk",
|
| 4 |
+
"agency": "FCA (Financial Conduct Authority)",
|
| 5 |
+
"title": "Collective Investment Schemes Regime for Tokenized Property",
|
| 6 |
+
"summary": "FCA regulatory framework for tokenized real estate under collective investment schemes (CIS) regime and Financial Services and Markets Act 2000 (FSMA). Covers authorization requirements, prospectus rules, restrictions on retail promotion, and ongoing supervision. Applies to tokens representing fractional property ownership offered to UK investors.",
|
| 7 |
+
"status": "effective",
|
| 8 |
+
"announced_date": "2019-01-23",
|
| 9 |
+
"effective_date": "2019-01-23",
|
| 10 |
+
"last_updated": "2024-10-01",
|
| 11 |
+
"source_url": "https://www.fca.org.uk/publication/policy/ps19-22.pdf",
|
| 12 |
+
"full_text_url": "https://www.fca.org.uk/firms/financial-promotions-regime",
|
| 13 |
+
"crypto_activities_affected": [
|
| 14 |
+
"tokenization",
|
| 15 |
+
"collective-investment-schemes",
|
| 16 |
+
"securities-offering",
|
| 17 |
+
"custody",
|
| 18 |
+
"financial-promotions"
|
| 19 |
+
],
|
| 20 |
+
"tags": [
|
| 21 |
+
"collective-investment-schemes",
|
| 22 |
+
"real-estate",
|
| 23 |
+
"security-tokens",
|
| 24 |
+
"specified-investments",
|
| 25 |
+
"financial-promotions",
|
| 26 |
+
"prospectus",
|
| 27 |
+
"fca-authorization"
|
| 28 |
+
],
|
| 29 |
+
"requirements": [
|
| 30 |
+
{
|
| 31 |
+
"requirement": "Collective Investment Scheme (CIS) Classification Assessment",
|
| 32 |
+
"description": "Tokenized real estate structures representing pooled investments where investors do not have day-to-day control are likely 'collective investment schemes' (CIS) under FSMA Section 235. CIS characteristics: (1) participants pool contributions, (2) property acquired/managed as whole, (3) participants do not have day-to-day control, (4) profits/income pooled and shared. Must obtain legal opinion from UK solicitor confirming CIS status and regulatory treatment. Cost: £25K-50K. Alternative structures: direct property ownership tokens (not CIS) or unregulated alternative investment fund (but restricted to sophisticated/high net worth investors only).",
|
| 33 |
+
"mandatory": true,
|
| 34 |
+
"deadline_days": 0,
|
| 35 |
+
"estimated_cost_usd": {
|
| 36 |
+
"min": 31000,
|
| 37 |
+
"max": 62000,
|
| 38 |
+
"currency": "USD",
|
| 39 |
+
"notes": "UK law firm legal opinion on CIS classification and FSMA compliance roadmap (£25K-50K at £1.24/GBP)"
|
| 40 |
+
},
|
| 41 |
+
"severity": "critical",
|
| 42 |
+
"exemptions": ["Single property direct ownership tokens where investors have day-to-day control (not CIS)"]
|
| 43 |
+
},
|
| 44 |
+
{
|
| 45 |
+
"requirement": "FCA Authorization as CIS Operator or AIFM",
|
| 46 |
+
"description": "Operating a CIS requires FCA authorization as: (1) Operator of CIS (if UCITS-compliant fund), OR (2) Alternative Investment Fund Manager (AIFM) if Alternative Investment Fund (AIF). Real estate funds typically AIFs. AIFM authorization requires: £125K initial capital (full-scope) or £50K (sub-threshold), fit and proper senior managers, compliance and risk functions, depositaries, valuation procedures, AIFMD compliance. Application process: 6-12 months. Application fee: £25K. Annual fees: £10K-50K based on AUM.",
|
| 47 |
+
"mandatory": true,
|
| 48 |
+
"deadline_days": 0,
|
| 49 |
+
"estimated_cost_usd": {
|
| 50 |
+
"min": 155000,
|
| 51 |
+
"max": 310000,
|
| 52 |
+
"currency": "USD",
|
| 53 |
+
"notes": "£125K regulatory capital + £25K application fee + legal/consultancy (£50K-100K). Sub-threshold AIFM only £50K capital if AUM <£100M."
|
| 54 |
+
},
|
| 55 |
+
"severity": "critical",
|
| 56 |
+
"exemptions": [
|
| 57 |
+
"Sub-threshold AIFM (AUM <€100M for unleveraged funds OR <€500M for leveraged funds) - reduced capital and disclosure requirements",
|
| 58 |
+
"Marketing only to professional/high net worth investors (still need authorization but simplified process)"
|
| 59 |
+
]
|
| 60 |
+
},
|
| 61 |
+
{
|
| 62 |
+
"requirement": "Prospectus or Exempted Document",
|
| 63 |
+
"description": "Offers of CIS units to UK public require prospectus approved by FCA unless exempt. Prospectus must comply with UK Prospectus Regulation including: property details and independent valuations, financial information (3 years audited financials), risk factors (minimum 20 categories), management team, use of proceeds, token structure and rights, taxation. Prospectus review and approval: 3-6 months. Most tokenized property offerings use exemptions: (1) qualified investors only, (2) offer to <150 persons (excluding qualified investors), (3) minimum investment ≥£100K, (4) total consideration <€8M in 12 months. Exempt offers still require clear and compliant information memorandum.",
|
| 64 |
+
"mandatory": true,
|
| 65 |
+
"deadline_days": 60,
|
| 66 |
+
"estimated_cost_usd": {
|
| 67 |
+
"min": 12400,
|
| 68 |
+
"max": 124000,
|
| 69 |
+
"currency": "USD",
|
| 70 |
+
"notes": "Full prospectus: £80K-100K. Exempt offer information memorandum: £10K-30K"
|
| 71 |
+
},
|
| 72 |
+
"severity": "critical",
|
| 73 |
+
"exemptions": [
|
| 74 |
+
"Offers to professional/qualified investors only",
|
| 75 |
+
"Offers to <150 persons (excluding qualified investors) per 12 months",
|
| 76 |
+
"Minimum investment ≥£100,000 per investor",
|
| 77 |
+
"Total consideration <€8M in 12 months"
|
| 78 |
+
]
|
| 79 |
+
},
|
| 80 |
+
{
|
| 81 |
+
"requirement": "Financial Promotions Approval and Restrictions",
|
| 82 |
+
"description": "All financial promotions (marketing) for CIS units must be: (1) approved by FCA-authorized firm, OR (2) fall within exemption (e.g., to certified high net worth, sophisticated investors, or investment professionals only). Retail promotion of real estate tokens is PROHIBITED since October 2023 under FCA PS23/6 unless: (a) promoted as security token admitted to trading on recognized investment exchange, OR (b) issuer holds relevant FCA permissions. Promotion to retail without approval: criminal offense (up to 2 years imprisonment + unlimited fines). High net worth certification: individual net assets >£250K (excluding primary residence) OR income >£100K.",
|
| 83 |
+
"mandatory": true,
|
| 84 |
+
"deadline_days": 0,
|
| 85 |
+
"estimated_cost_usd": {
|
| 86 |
+
"min": 18600,
|
| 87 |
+
"max": 62000,
|
| 88 |
+
"currency": "USD",
|
| 89 |
+
"notes": "FCA-authorized firm approval fees (£5K-20K per campaign) + legal review (£10K-30K) + compliance procedures"
|
| 90 |
+
},
|
| 91 |
+
"severity": "critical",
|
| 92 |
+
"exemptions": [
|
| 93 |
+
"Promotions to certified high net worth individuals (net assets >£250K or income >£100K)",
|
| 94 |
+
"Promotions to certified sophisticated investors (self-certified knowledge and experience)",
|
| 95 |
+
"Promotions to investment professionals only"
|
| 96 |
+
]
|
| 97 |
+
},
|
| 98 |
+
{
|
| 99 |
+
"requirement": "Depositary Appointment (AIFMD Requirement)",
|
| 100 |
+
"description": "AIFs must appoint eligible depositary (credit institution, MiFID investment firm, or authorized AIFM depositary) to: (1) hold scheme assets or verify ownership, (2) monitor cash flows, (3) oversight of valuation, (4) carry out depositary's instructions unless unlawful. Depositary liable for loss of financial instruments held in custody. For tokenized real estate: depositary holds property title deeds and oversees token ledger integrity. Depositary fees: 0.02-0.10% of NAV annually (minimum £25K-50K annually). Depositary agreement and appointment required before fund launch.",
|
| 101 |
+
"mandatory": true,
|
| 102 |
+
"deadline_days": 90,
|
| 103 |
+
"estimated_cost_usd": {
|
| 104 |
+
"min": 31000,
|
| 105 |
+
"max": 124000,
|
| 106 |
+
"currency": "USD",
|
| 107 |
+
"notes": "Annual depositary fees (£25K-100K depending on NAV) + setup and legal agreements (£10K-20K)"
|
| 108 |
+
},
|
| 109 |
+
"severity": "high",
|
| 110 |
+
"exemptions": ["Sub-threshold AIFMs with simplified AIFMD compliance may have reduced depositary requirements"]
|
| 111 |
+
},
|
| 112 |
+
{
|
| 113 |
+
"requirement": "Independent Valuation and Valuer Appointment",
|
| 114 |
+
"description": "Real estate assets must be independently valued: (1) before initial investment or property acquisition, (2) at least annually thereafter, (3) when material event affects valuation (damage, rezoning, market changes >10%). Valuer must be: (a) independent external valuer (e.g., RICS-qualified surveyor), OR (b) internal valuer functionally independent of portfolio management. Valuation must follow RICS Red Book standards. Valuation reports must be provided to depositary and disclosed to investors. Valuation frequency for daily-traded funds: monthly. Cost: £5K-30K per property per valuation.",
|
| 115 |
+
"mandatory": true,
|
| 116 |
+
"deadline_days": 90,
|
| 117 |
+
"estimated_cost_usd": {
|
| 118 |
+
"min": 12400,
|
| 119 |
+
"max": 62000,
|
| 120 |
+
"currency": "USD",
|
| 121 |
+
"notes": "Initial valuations (£10K-30K per property) + annual revaluations (£5K-20K). Multi-property portfolios: £50K-100K annually."
|
| 122 |
+
},
|
| 123 |
+
"severity": "high",
|
| 124 |
+
"exemptions": []
|
| 125 |
+
},
|
| 126 |
+
{
|
| 127 |
+
"requirement": "Senior Managers and Certification Regime (SMCR) Compliance",
|
| 128 |
+
"description": "AIFM must comply with SMCR including: (1) identify and allocate Senior Management Functions (SMFs) - e.g., CEO, compliance oversight, money laundering reporting officer (MLRO), (2) obtain regulatory approval for SMF appointments (3-6 months), (3) certify other staff performing Certification Functions annually, (4) implement Conduct Rules for all staff, (5) maintain records (responsibilities maps, handover procedures). Fit and proper assessments for all SMFs. Penalties for SMCR breaches: up to £1M per individual + prohibition orders.",
|
| 129 |
+
"mandatory": true,
|
| 130 |
+
"deadline_days": 120,
|
| 131 |
+
"estimated_cost_usd": {
|
| 132 |
+
"min": 31000,
|
| 133 |
+
"max": 93000,
|
| 134 |
+
"currency": "USD",
|
| 135 |
+
"notes": "First year: SMCR gap analysis + SMF approval applications (£10K-30K) + governance framework + training (£15K-45K)"
|
| 136 |
+
},
|
| 137 |
+
"severity": "high",
|
| 138 |
+
"exemptions": []
|
| 139 |
+
},
|
| 140 |
+
{
|
| 141 |
+
"requirement": "Anti-Money Laundering (AML) and Counter-Terrorism Financing (CTF)",
|
| 142 |
+
"description": "Must comply with Money Laundering Regulations 2017 (MLR 2017) including: (1) risk-based approach and enterprise-wide risk assessment, (2) customer due diligence (CDD) - verify identity and source of funds, (3) enhanced due diligence (EDD) for PEPs and high-risk customers (investments >£10K), (4) ongoing monitoring, (5) suspicious activity reports (SARs) to National Crime Agency (NCA), (6) sanctions screening (OFSI, EU, UN lists), (7) appoint nominated officer (Money Laundering Reporting Officer), (8) staff training (annual), (9) record retention (5 years). Independent AML audit every 2 years.",
|
| 143 |
+
"mandatory": true,
|
| 144 |
+
"deadline_days": 90,
|
| 145 |
+
"estimated_cost_usd": {
|
| 146 |
+
"min": 37200,
|
| 147 |
+
"max": 93000,
|
| 148 |
+
"currency": "USD",
|
| 149 |
+
"notes": "First year: AML/CTF system (£20K-50K) + MLRO appointment + training + screening tools. Annual ongoing: £15K-40K"
|
| 150 |
+
},
|
| 151 |
+
"severity": "critical",
|
| 152 |
+
"exemptions": []
|
| 153 |
+
},
|
| 154 |
+
{
|
| 155 |
+
"requirement": "Ongoing Reporting and Disclosure Obligations",
|
| 156 |
+
"description": "Authorized AIFMs must provide: (1) annual report to investors within 6 months of year-end (audited accounts, valuation reports, remuneration disclosure), (2) semi-annual reports (if required by fund rules), (3) quarterly investor updates on property performance, (4) AIFMD Annex IV reporting to FCA (annually or quarterly if AUM >€1B), (5) material event notifications within 14 days, (6) respond to investor information requests within reasonable time. Retail funds: additional COLL Sourcebook disclosure requirements. Failure to report triggers supervisory action.",
|
| 157 |
+
"mandatory": true,
|
| 158 |
+
"deadline_days": 0,
|
| 159 |
+
"estimated_cost_usd": {
|
| 160 |
+
"min": 31000,
|
| 161 |
+
"max": 93000,
|
| 162 |
+
"currency": "USD",
|
| 163 |
+
"notes": "Annual ongoing: external audit (£20K-50K) + valuations + investor reporting systems + compliance staff (£25K-75K)"
|
| 164 |
+
},
|
| 165 |
+
"severity": "medium",
|
| 166 |
+
"exemptions": []
|
| 167 |
+
},
|
| 168 |
+
{
|
| 169 |
+
"requirement": "Operational Resilience and Cybersecurity",
|
| 170 |
+
"description": "Must comply with FCA operational resilience requirements (effective March 2022) including: (1) identify important business services (e.g., token custody, investor reporting), (2) set impact tolerances (maximum disruption before unacceptable harm), (3) mapping and testing (scenario testing, including severe but plausible disruptions), (4) communication plans, (5) self-assessment annually. Cybersecurity: implement controls aligned with NIST/ISO 27001, penetration testing (annually), incident response plan, data encryption. Operational incidents must be reported to FCA within defined timeframes (critical: immediately).",
|
| 171 |
+
"mandatory": true,
|
| 172 |
+
"deadline_days": 180,
|
| 173 |
+
"estimated_cost_usd": {
|
| 174 |
+
"min": 37200,
|
| 175 |
+
"max": 124000,
|
| 176 |
+
"currency": "USD",
|
| 177 |
+
"notes": "First year: operational resilience framework (£15K-40K) + cybersecurity assessment + pen testing + BCP (£15K-60K)"
|
| 178 |
+
},
|
| 179 |
+
"severity": "high",
|
| 180 |
+
"exemptions": []
|
| 181 |
+
},
|
| 182 |
+
{
|
| 183 |
+
"requirement": "Secondary Market and Transfer Restrictions",
|
| 184 |
+
"description": "If providing secondary market for CIS units/tokens, must either: (1) list on FCA-recognized investment exchange (e.g., LSE, Aquis), OR (2) operate as Multilateral Trading Facility (MTF) or Organized Trading Facility (OTF) - requires MiFID investment firm authorization (£50K-500K costs). Most tokenized real estate offerings restrict transfers: (a) 12-month lock-up, (b) transfers only with manager approval, (c) minimum holding period. Smart contract must enforce transfer restrictions. Unlisted CIS units difficult to transfer - liquidity risk must be disclosed prominently.",
|
| 185 |
+
"mandatory": false,
|
| 186 |
+
"deadline_days": 0,
|
| 187 |
+
"estimated_cost_usd": {
|
| 188 |
+
"min": 18600,
|
| 189 |
+
"max": 620000,
|
| 190 |
+
"currency": "USD",
|
| 191 |
+
"notes": "If exchange listing: £50K-150K setup + ongoing fees. If operate MTF: £100K-500K+ for MiFID authorization. If restricted transfers: £15K legal documentation."
|
| 192 |
+
},
|
| 193 |
+
"severity": "medium",
|
| 194 |
+
"exemptions": ["Transfers restricted to original investors or with manager approval - no MTF/exchange needed"]
|
| 195 |
+
}
|
| 196 |
+
],
|
| 197 |
+
"penalties": [
|
| 198 |
+
{
|
| 199 |
+
"violation": "Operating Unauthorized CIS or AIFM",
|
| 200 |
+
"penalty_type": "Criminal Prosecution + Unlimited Fines",
|
| 201 |
+
"amount_usd": {
|
| 202 |
+
"min": 0,
|
| 203 |
+
"max": 99999999,
|
| 204 |
+
"notes": "Criminal offense under FSMA Section 23: unlimited fines + imprisonment up to 2 years + investor restitution orders + disgorgement of all fees"
|
| 205 |
+
},
|
| 206 |
+
"additional_consequences": [
|
| 207 |
+
"Criminal conviction and imprisonment (up to 2 years)",
|
| 208 |
+
"Unlimited fines (no statutory cap)",
|
| 209 |
+
"Investor restitution orders (full refund of investments)",
|
| 210 |
+
"Director disqualification (2-15 years)",
|
| 211 |
+
"Permanent ban from UK financial services",
|
| 212 |
+
"Asset freezing and restraint orders"
|
| 213 |
+
]
|
| 214 |
+
},
|
| 215 |
+
{
|
| 216 |
+
"violation": "Illegal Financial Promotion to Retail Investors",
|
| 217 |
+
"penalty_type": "Criminal Prosecution + Civil Penalties",
|
| 218 |
+
"amount_usd": {
|
| 219 |
+
"min": 0,
|
| 220 |
+
"max": 99999999,
|
| 221 |
+
"notes": "Criminal offense under FSMA Section 21: unlimited fines + imprisonment up to 2 years + FCA civil penalties up to £1M or higher of disgorgement"
|
| 222 |
+
},
|
| 223 |
+
"additional_consequences": [
|
| 224 |
+
"Criminal prosecution (up to 2 years imprisonment)",
|
| 225 |
+
"Unlimited criminal fines",
|
| 226 |
+
"FCA public censure and financial penalties",
|
| 227 |
+
"Investor compensation orders",
|
| 228 |
+
"Prohibition orders preventing industry participation"
|
| 229 |
+
]
|
| 230 |
+
},
|
| 231 |
+
{
|
| 232 |
+
"violation": "Misleading Prospectus or Material Omissions",
|
| 233 |
+
"penalty_type": "Criminal Prosecution + Civil Liability",
|
| 234 |
+
"amount_usd": {
|
| 235 |
+
"min": 62000,
|
| 236 |
+
"max": 99999999,
|
| 237 |
+
"notes": "Criminal liability under FSMA: unlimited fines + up to 7 years imprisonment + civil compensation to investors who relied on prospectus"
|
| 238 |
+
},
|
| 239 |
+
"additional_consequences": [
|
| 240 |
+
"Criminal prosecution under FSMA s.90/s.397 (up to 7 years imprisonment)",
|
| 241 |
+
"Civil liability to all investors (rescission + damages)",
|
| 242 |
+
"FCA enforcement action and fines",
|
| 243 |
+
"Director personal liability",
|
| 244 |
+
"Fraud Act 2006 prosecution if dishonest"
|
| 245 |
+
]
|
| 246 |
+
},
|
| 247 |
+
{
|
| 248 |
+
"violation": "AML/CTF Breaches or Inadequate Controls",
|
| 249 |
+
"penalty_type": "Criminal Prosecution + Civil Penalties",
|
| 250 |
+
"amount_usd": {
|
| 251 |
+
"min": 124000,
|
| 252 |
+
"max": 99999999,
|
| 253 |
+
"notes": "Criminal penalties under MLR 2017: unlimited fines + imprisonment up to 2 years + FCA financial penalties (up to £5M or higher for serious breaches) + authorization withdrawal"
|
| 254 |
+
},
|
| 255 |
+
"additional_consequences": [
|
| 256 |
+
"Criminal prosecution (up to 2 years imprisonment)",
|
| 257 |
+
"FCA authorization revocation or suspension",
|
| 258 |
+
"Serious Fraud Office (SFO) investigation if money laundering facilitated",
|
| 259 |
+
"SMCR prohibition orders for senior managers",
|
| 260 |
+
"Enhanced supervision and mandatory remediation"
|
| 261 |
+
]
|
| 262 |
+
}
|
| 263 |
+
],
|
| 264 |
+
"regulatory_guidance": [
|
| 265 |
+
"FCA PS19/22 (2019) confirms cryptoassets representing security tokens are regulated under FSMA",
|
| 266 |
+
"Most tokenized real estate structures are CIS under Section 235 FSMA - requires authorization",
|
| 267 |
+
"FCA PS23/6 (October 2023) banned retail promotion of most cryptoassets including real estate tokens - high net worth/sophisticated investors only",
|
| 268 |
+
"Real estate tokens typically classified as 'units in collective investment scheme' or 'alternative investment fund'",
|
| 269 |
+
"FCA Perimeter Guidance PERG 9 provides guidance on CIS classification",
|
| 270 |
+
"Direct property ownership tokens (where investor has day-to-day control) may escape CIS classification but rare in practice",
|
| 271 |
+
"UK regulatory approach conservative compared to Singapore/Switzerland - retail access highly restricted",
|
| 272 |
+
"Brexit impact: UK Prospectus Regulation post-Brexit allows offers <£8M without prospectus (previously €8M)"
|
| 273 |
+
],
|
| 274 |
+
"related_regulations": [
|
| 275 |
+
"uk-fca-cryptoasset-promotion-ban-2023",
|
| 276 |
+
"uk-fca-smcr-2024",
|
| 277 |
+
"uk-mlr-aml-2024",
|
| 278 |
+
"uk-fca-operational-resilience-2022"
|
| 279 |
+
],
|
| 280 |
+
"confidence": 0.93,
|
| 281 |
+
"notes": "UK's regulatory framework for tokenized real estate is stringent and protective of retail investors. Post-October 2023, retail promotion of real estate tokens effectively banned unless listed on recognized exchange. Total first-year compliance costs for UK real estate token offering: £500K-1.2M (USD $620K-1.5M). Ongoing annual costs: £150K-400K. Most cost-effective approach: market only to high net worth/sophisticated investors (reduces prospectus and authorization complexity). Full retail authorization path extremely expensive and time-consuming (12-24 months)."
|
| 282 |
+
}
|