DEV Community

Cover image for GlobalScreen API - Production-Ready Sanctions & PEP Screening Service
Mayuresh Smita Suresh
Mayuresh Smita Suresh Subscriber

Posted on

GlobalScreen API - Production-Ready Sanctions & PEP Screening Service

Xano AI-Powered Backend Challenge: Public API Submission

This is a submission for the Xano AI-Powered Backend Challenge: Production-Ready Public API

What I Built

GlobalScreen API is a production-ready sanctions and PEP (Politically Exposed Persons) screening service that enables third-party applications to perform compliance checks against international watchlists. Think of it as Stripe for compliance - a simple API that solves a complex regulatory problem.

🎯 Core Features

  • Real UN Sanctions Data: Integrated 1,000+ verified entries from the UN Consolidated Sanctions List
  • Smart Fuzzy Matching: AI-powered search finds matches even with typos or name variations
  • Comprehensive Coverage: Both individuals (727 entries) and entities (273 entries) across 10+ sanctions programs
  • Production Security: OAuth 2.0 Bearer token authentication with tiered rate limiting
  • Full Audit Trail: Complete logging of all screening requests for compliance reporting
  • Developer-Friendly: RESTful design with clear error messages and pagination

πŸ’Ό Real-World Use Cases

  • Fintech KYC/AML: Customer onboarding verification for banks and payment processors
  • E-commerce: Transaction monitoring for high-risk merchants
  • HR & Recruitment: Background checks for politically exposed persons
  • Real Estate: Due diligence for property transactions
  • Legal/Consulting: Client intake screening

API Documentation

πŸ“‘ Base URL

https://xvln-hqrc-qxwa.n7e.xano.io/api:QC35j52Y
Enter fullscreen mode Exit fullscreen mode

πŸ” Authentication

All endpoints require Bearer token authentication:

# 1. Sign up to create an account
POST /auth/signup
{
  "email": "user@example.com",
  "name": "John Doe",
  "password": "secure-password"
}

# 2. Login to get your Bearer token
POST /auth/login
{
  "email": "user@example.com",
  "password": "secure-password"
}

# Response includes your token
{
  "authToken": "eyJhbGciOiJBMjU2S1ciLC..."
}

# 3. Use token in all API calls
Authorization: Bearer eyJhbGciOiJBMjU2S1ciLC...
Enter fullscreen mode Exit fullscreen mode

πŸ“Š Rate Limits 1000 per account

Rate limit info is included in response headers:

  • X-RateLimit-Limit: Maximum requests allowed
  • X-RateLimit-Remaining: Requests remaining in current window
  • X-RateLimit-Reset: Unix timestamp when limit resets

πŸ”Ž Endpoint 1: GET /screening_entity

Search for a specific person or entity by name with fuzzy matching.

Request:

curl -X GET 'https://xvln-hqrc-qxwa.n7e.xano.io/api:QC35j52Y/search' \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "ERIC BADEGE",
    "entity_type": "Individual",
    "nationality": "CD"
  }'
Enter fullscreen mode Exit fullscreen mode

Parameters:

  • name (required): Full or partial name to search (2-200 characters)
  • entity_type (optional): Filter by "Individual" or "Entity"
  • nationality (optional): 2-letter ISO country code

Response:

{
  "success": true,
  "data": {
    "query": "ERIC BADEGE",
    "total_matches": 1,
    "results": [
      {
        "id": 1,
        "name": "ERIC BADEGE",
        "name_normalized": "eric badege",
        "entity_type": "Individual",
        "program": "UN - DRC",
        "nationality": "CD",
        "date_of_birth": "1971-01-01",
        "address": "Rwanda",
        "risk_level": "HIGH",
        "status": "ACTIVE",
        "match_score": 100,
        "entity_number": "CDi.001",
        "remarks": "He fled to Rwanda in March 2013...",
        "list_date": "2012-12-31"
      }
    ]
  },
  "meta": {
    "timestamp": "2024-12-14T10:30:00Z",
    "rate_limit": {
      "remaining": 999,
      "reset_at": 1734177600
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Match Scoring:

  • 100: Exact match
  • 90: Name starts with search term
  • 85: Match found in aliases (AKA field)
  • 80: Partial match

πŸ“‹ Endpoint 2: GET /list

Get paginated list of watchlist entries with optional filters.

Request:

curl 'https://xvln-hqrc-qxwa.n7e.xano.io/api:QC35j52Y/screen_entity?page=1&per_page=20&entity_type=Individual&program=Al-Qaida' \
  -H 'Authorization: Bearer YOUR_TOKEN'
Enter fullscreen mode Exit fullscreen mode

Query Parameters:

  • name (optional): Filter by name (partial matching)
  • page (optional, default: 1): Page number
  • per_page (optional, default: 20, max: 100): Results per page
  • entity_type (optional): "Individual" or "Entity"
  • nationality (optional): 2-letter country code
  • program (optional): Sanctions program name
  • risk_level (optional): "HIGH", "MEDIUM", "LOW"
  • status (optional, default: "ACTIVE"): "ACTIVE" or "INACTIVE"

Response:

{
  "success": true,
  "data": [
    {
      "id": 1,
      "name": "ERIC BADEGE",
      "entity_type": "Individual",
      "program": "UN - DRC",
      "nationality": "CD",
      "risk_level": "HIGH",
      "status": "ACTIVE"
    },
    // ... more entries
  ],
  "meta": {
    "page": 1,
    "per_page": 20,
    "total_results": 1000,
    "total_pages": 50,
    "has_next": true,
    "has_previous": false
  }
}
Enter fullscreen mode Exit fullscreen mode

❌ Error Responses

All errors follow a consistent format:

{
  "success": false,
  "error": {
    "code": "UNAUTHORIZED",
    "message": "Invalid or missing Authorization header"
  },
  "meta": {
    "timestamp": "2024-12-14T10:30:00Z"
  }
}
Enter fullscreen mode Exit fullscreen mode

Error Codes:

  • 400 BAD_REQUEST: Missing or invalid parameters
  • 401 UNAUTHORIZED: Missing or invalid Bearer token
  • 403 FORBIDDEN: Token is inactive or insufficient permissions
  • 429 RATE_LIMIT_EXCEEDED: Too many requests
  • 500 INTERNAL_SERVER_ERROR: Server error

πŸ“š Data Coverage

Sanctions Programs:

  • UN - Al-Qaida (341 entries, 34%)
  • UN - DPRK (North Korea) (155 entries, 15%)
  • UN - Taliban (140 entries, 14%)
  • UN - Iran (121 entries, 12%)
  • UN - Iraq (76 entries, 8%)
  • Plus: DRC, Libya, Somalia, CAR, Haiti

Top Countries:

  • Afghanistan (AF): 129 entries
  • Iraq (IQ): 80 entries
  • Indonesia (ID): 23 entries
  • Democratic Republic of Congo (CD): 22 entries
  • Tunisia (TN): 21 entries

Demo

🎬 Live Demo Screenshots

** Match Search**
my output

Search for "ERIC" finds "ERIC BADEGE" with 90 match score - demonstrates partial name matching capability.

** Paginated List View**

List

** Xano Database View**

xano database view

1,000 verified UN sanctions records in watchlist_entries table, properly normalized and indexed for fast searches.


πŸ§ͺ Test Cases

You can test the API with these verified names from the database:

Individuals:

# Exact match test
curl -X POST '.../search' -H 'Authorization: Bearer TOKEN' \
  -d '{"name": "ERIC BADEGE"}'

# Partial match test
curl -X POST '.../search' -H 'Authorization: Bearer TOKEN' \
  -d '{"name": "GASTON"}'

# Alias match test (searches AKA field)
curl -X POST '.../search' -H 'Authorization: Bearer TOKEN' \
  -d '{"name": "AIGLE BLANC"}'
Enter fullscreen mode Exit fullscreen mode

Entities:

# Entity search
curl -X POST '.../search' -H 'Authorization: Bearer TOKEN' \
  -d '{"name": "HAWALA", "entity_type": "Entity"}'
Enter fullscreen mode Exit fullscreen mode

Filtered Listing:

# Get Al-Qaida individuals only
curl '.../screen_entity?entity_type=Individual&program=Al-Qaida' \
  -H 'Authorization: Bearer TOKEN'
Enter fullscreen mode Exit fullscreen mode

The AI Prompt I Used

I started with Xano's AI assistant using this prompt:

Create a production-ready sanctions screening API called "GlobalScreen" with these requirements:

DATABASE SCHEMA:
Create 4 tables:

1. users (authentication)
   - id, email, password, name, company
   - rate_limit, requests_count, rate_limit_reset
   - created_at, updated_at

2. watchlist_entries (1,000 UN sanctions records)
   - id, source, entity_type, name, name_normalized
   - aka (aliases), program, nationality, date_of_birth
   - place_of_birth, address, position, country
   - remarks, list_date, entity_number
   - risk_level, status, created_at, updated_at

3. screening_logs (audit trail)
   - id, user_id, search_type, search_name
   - match_count, created_at, ip_address

4. api_keys (was later removed - using Bearer tokens instead)

API ENDPOINTS:

1. POST /search
   - Search by name with fuzzy matching
   - Optional filters: entity_type, nationality
   - Return match_score for each result
   - Limit to 20 top matches

2. GET /screen_entity
   - Paginated list of watchlist entries
   - Filters: name, entity_type, nationality, program, risk_level, status
   - Default: 20 per page, max 100

AUTHENTICATION:
- Bearer token (OAuth 2.0 style)
- Rate limiting by tier (100/1000/10000 per hour)
- Proper error responses (401, 403, 429)

FUZZY MATCHING:
- Search both name_normalized and aka fields
- Case-insensitive partial matching
- Calculate match scores (100=exact, 90=starts with, 80=contains, 85=aka match)
- Sort by match_score DESC

AUDIT LOGGING:
- Log every search request
- Track: user_id, search_name, match_count, timestamp, IP

Please implement with proper validation, error handling, and production-ready code.
Enter fullscreen mode Exit fullscreen mode

This prompt generated about 70% of the backend. The AI created:

  • βœ… Database schema with all tables
  • βœ… Basic authentication flow
  • βœ… Endpoint structure
  • βœ… Input validation
  • βœ… Error response format

How I Refined the AI-Generated Code

The AI gave me a solid foundation, but I made several critical improvements for production readiness:

πŸ”§ Fix #1: Fuzzy Matching Logic

BEFORE (AI-Generated):

// AI used exact match with WHERE clause
db.query watchlist_entries {
  where = ($db.watchlist_entries.name == $input.name)
  return = {type: "list"}
}
Enter fullscreen mode Exit fullscreen mode

AFTER (My Improvement):

// Query all active entries, then filter in memory
db.query watchlist_entries {
  return = {type: "list"}
} as $all_entries

// Normalize search term
var $search_term {
  value = $input.name|to_lower|trim
}

// Filter with partial matching
foreach ($all_entries) {
  each as $entry {
    // Check if name_normalized contains search term
    conditional {
      if ($entry.name_normalized|contains:$search_term) {
        // Add to results with match score
      }
    }

    // Also check aka field for aliases
    conditional {
      if ($entry.aka|to_lower|contains:$search_term) {
        // Higher priority for alias matches
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Why: Xano's AI couldn't handle complex WHERE clauses with partial matching and OR logic. My solution uses in-memory filtering which is more flexible and actually works.


πŸ”§ Fix #2: Match Score Calculation

BEFORE (AI-Generated):

// No match scoring in AI version
// Just returned raw results
Enter fullscreen mode Exit fullscreen mode

AFTER (My Improvement):

foreach ($raw_results) {
  each as $entry {
    var $match_score {
      value = 80  // default
    }

    // Exact match
    conditional {
      if ($entry.name_normalized == $search_term) {
        var.update $match_score { value = 100 }
      }
    }

    // Starts with
    conditional {
      if ($entry.name_normalized|starts_with:$search_term) {
        var.update $match_score { value = 90 }
      }
    }

    // Alias match
    conditional {
      if ($entry.aka|to_lower|contains:$search_term) {
        var.update $match_score { value = 85 }
      }
    }

    // Add score to result
    var $result_with_score {
      value = $entry|set:"match_score":$match_score
    }
  }
}

// Sort by score descending
var $sorted_results {
  value = $results_with_scores|sort:"match_score":"int":true|slice:0:20
}
Enter fullscreen mode Exit fullscreen mode

Why: Fuzzy matching is useless without confidence scores. This helps developers decide if a match is worth flagging.


πŸ”§ Fix #3: Rate Limiting Implementation

BEFORE (AI-Generated):

// AI created basic check but didn't reset properly
precondition ($user.requests_count < $user.rate_limit) {
  error = "Rate limit exceeded"
}
Enter fullscreen mode Exit fullscreen mode

AFTER (My Improvement):

// Get current timestamp
var $current_time { value = now }

// Check if rate limit window has expired
conditional {
  if ($user.rate_limit_reset == null || $current_time > $user.rate_limit_reset) {
    // Reset counter for new hour
    var $new_reset_time {
      value = $current_time + 3600000  // +1 hour in milliseconds
    }

    db.edit user {
      field_name = "id"
      field_value = $auth.id
      data = {
        requests_count: 0,
        rate_limit_reset: $new_reset_time
      }
    }
  }
}

// Now check limit
precondition ($user.requests_count < $user.rate_limit) {
  error_type = "inputerror"
  error = "Rate limit exceeded"
}

// Increment counter
db.edit user {
  field_name = "id"
  field_value = $auth.id
  data = { requests_count: ($user.requests_count + 1) }
}
Enter fullscreen mode Exit fullscreen mode

Why: The AI version would never reset the counter, so users would hit the limit once and be locked out forever!


πŸ”§ Fix #4: Error Response Consistency

BEFORE (AI-Generated):

// AI returned different error formats
return { error: "something went wrong" }
// or
return { message: "error" }
// or
return { code: 400 }
Enter fullscreen mode Exit fullscreen mode

AFTER (My Improvement):

// Standardized error format everywhere
return {
  value = {
    success: false,
    error: {
      code: $error_code,
      message: $error.message|first_notempty:"Error occurred"
    },
    meta: {
      timestamp: $current_time|format_timestamp:"Y-m-d\TH:i:s\Z":"UTC"
    }
  }
}

// With proper HTTP status codes
conditional {
  if ($error.message|contains:"Rate limit") {
    var.update $error_code { value = "RATE_LIMIT_EXCEEDED" }
    var.update $http_status { value = 429 }
  }
}
Enter fullscreen mode Exit fullscreen mode

Why: Consistent error format makes it easy for developers to handle errors programmatically. The AI mixed different formats.


πŸ”§ Fix #5: Manual Pagination for Filtered Results

BEFORE (AI-Generated):

// AI tried to use database pagination with WHERE clause
db.query watchlist_entries {
  where = $complex_expression
  return = {
    type: "list",
    paging: { page: $input.page, per_page: $input.per_page }
  }
}
Enter fullscreen mode Exit fullscreen mode

AFTER (My Improvement):

// Since we filter in memory, paginate manually
var $total_count {
  value = $matched_results|count
}

var $total_pages {
  value = ($total_count / $input.per_page)|ceil
}

var $offset {
  value = ($input.page - 1) * $input.per_page
}

var $paginated_results {
  value = $matched_results|slice:$offset:$input.per_page
}

var $meta_data {
  value = {
    page: $input.page,
    per_page: $input.per_page,
    total_results: $total_count,
    total_pages: $total_pages,
    has_next: $input.page < $total_pages,
    has_previous: $input.page > 1
  }
}
Enter fullscreen mode Exit fullscreen mode

Why: Xano's AI couldn't combine complex WHERE clauses with pagination. Manual pagination works perfectly.


Custom Data Import & Cleaning Function in Xano

**
AI Didn't Do This At All - Built Entirely From Scratch in Xano:
**

The biggest challenge wasn't the API endpointsβ€”it was getting 1,000+ UN sanctions records cleaned, normalized, and imported into Xano. The raw UN data had inconsistent formatting, mixed cases, and missing fields. I built a custom Xano function to process and validate CSV data before import.

πŸ“Š Summary of Improvements

Aspect AI Generated My Improvements
Fuzzy Matching ❌ Exact only βœ… Partial + aliases
Match Scoring ❌ None βœ… 80-100 scale
Rate Limiting ⚠️ Broken (no reset) βœ… Hourly windows
Errors ⚠️ Inconsistent βœ… Standardized format
Pagination ⚠️ Doesn't work with filters βœ… Manual pagination
Data Import ❌ Not provided βœ… Complete pipeline
Case Sensitivity ❌ Fails on "eric" vs "ERIC" βœ… Normalized search
Alias Search ❌ Name only βœ… Name + aka fields

Time Saved by AI: ~40% (basic structure, auth, validation)
Time Spent Fixing: ~60% (all the critical production features)


My Experience with Xano

βœ… What I Loved

1. Visual Query Builder
The visual database query builder is fantastic for simple queries. Click, drag, done. Way faster than writing SQL.

2. Built-in Authentication
Xano's auth system with Bearer tokens worked out of the box. No need to implement JWT signing/verification myself.

3. Function Stack Concept
The visual "function stack" makes it easy to see the flow of data through your endpoint. Great for debugging.

4. Instant API Docs
Xano auto-generates API documentation from your endpoints. Saved hours compared to writing Swagger specs.

5. Fast Iteration
Changes deploy instantly. No build/compile step. Made testing and iteration very fast.


πŸ˜… The Challenges

1. Complex WHERE Clauses Don't Work

// This looks right but throws cryptic errors
where = ($db.table.field includes $var || $db.table.field2 == $var2)
Enter fullscreen mode Exit fullscreen mode

Solution: Query everything, filter in memory with foreach loops. Less efficient but actually works.

2. AI Generates Invalid Syntax
Xano's AI often generated code that looked correct but had subtle syntax errors:

  • Using filter instead of return
  • Using contains in WHERE clauses (not supported)
  • Missing proper operator precedence

Solution: Learn Xano's actual syntax.

3. Pagination + Filtering = Manual Work
Can't combine database pagination with complex filters. Had to implement manual pagination with slice.

4. Debugging is Hard
Error messages like "Missing var entry: api_key_record" don't tell you which line failed. Had to add lots of debugging output.

5. No Local Development
Everything is in the cloud. No way to run Xano locally or use version control effectively.


πŸ’‘ Tips for Others

1. Start Simple, Then Refine
Let the AI generate a basic version, then manually improve it. Don't expect perfection from AI.

2. Test Every Change
With no local dev environment, test in production. Make small changes and test immediately.

3. Use Foreach Loops for Complex Filtering
If your WHERE clause gets complex, just query all and filter in memory. More reliable.

4. Read the Docs
Xano's syntax is specific. The AI doesn't always know the latest syntax. Check official docs.

5. Keep Functions Small
Break complex logic into multiple small functions. Easier to debug when things break.


🎯 Final Verdict

Would I use Xano again? Yes, for rapid prototyping and MVPs.

Would I use it for production? Maybe, with reservations:

  • βœ… Great for: REST APIs with simple queries
  • ⚠️ Okay for: Medium complexity business logic
  • ❌ Not for: Complex data transformations, high-performance needs

Best Feature: Speed of development. I built this in 3 days that would've taken 2 weeks with traditional backend.

Biggest Limitation: Complex queries require workarounds. Miss the power of raw SQL.

Overall Experience: 7/10. Powerful but has rough edges. The AI helps but isn't magic - you still need to know what you're doing.


πŸš€ Try It Yourself

The API is live and ready to use!

Quick Start:

# 1. Sign up
curl -X POST 'https://xvln-hqrc-qxwa.n7e.xano.io/api:QC35j52Y/auth/signup' \
  -H 'Content-Type: application/json' \
  -d '{"email": "you@example.com", "name": "Your Name", "password": "secure123"}'

# 2. Login
curl -X POST 'https://xvln-hqrc-qxwa.n7e.xano.io/api:QC35j52Y/auth/login' \
  -H 'Content-Type: application/json' \
  -d '{"email": "you@example.com", "password": "secure123"}'

# 3. Search (use the token from login response)
curl -X POST 'https://xvln-hqrc-qxwa.n7e.xano.io/api:QC35j52Y/search' \
  -H 'Authorization: Bearer YOUR_TOKEN_HERE' \
  -H 'Content-Type: application/json' \
  -d '{"name": "ERIC BADEGE"}'
Enter fullscreen mode Exit fullscreen mode

Free tier: 100 requests/hour - plenty for testing!


πŸ“ˆ Future Enhancements

If I had more time, I'd add:

  1. More Data Sources: OFAC, EU sanctions, PEP databases
  2. Webhooks: Real-time notifications when watchlist updates
  3. Bulk Screening: Upload CSV of 1,000 names, get results
  4. Historical Tracking: See when someone was added/removed from lists
  5. Risk Scoring ML: Machine learning to improve match confidence
  6. SDKs: Python, JavaScript, PHP client libraries

πŸ™ Acknowledgments

  • UN Security Council for maintaining the Consolidated Sanctions List
  • Xano team for the AI-powered backend platform
  • DEV Community for running this challenge

πŸ“Š Stats

  • Lines of Code: ~1,200 (Xano script)
  • Development Time: 3 days
  • Database Records: 1,000 verified entries
  • API Endpoints: 4 (signup, login, search, list)
  • Test Coverage: 15+ test cases
  • Countries Covered: 50+ nationalities
  • Sanctions Programs: 10+ UN programs

Built with ❀️ using Xano, real UN data, and a lot of debugging!
A comprehensive compliance API built with Xano, featuring real UN sanctions data, AI-powered fuzzy matching, and production-grade security - Dataset Download here limit to 1000 entries.

Top comments (2)

Collapse
 
pegasusryug20_2d6ee71068a profile image
Pegasusryug20

great, I didn't even know UN dataset and can be used like this.

Collapse
 
mayu2008 profile image
Mayuresh Smita Suresh

Thank you!!!