# Project export: fake qr code detector

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: OpenAI Build Week
- Tagline: scan a qr code. show the destination before opening detect suspicious domains warn users about phishing
- Devpost: https://devpost.com/software/fake-qr-code-detector
- GitHub: https://github.com/Jaswanth618/fake_qr-detector
- Video: https://www.youtube.com/embed/LdxyZSd3ZM0?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — jaswanth kumar (1 commits)

## Devpost submission (written by the team)

### Challenges we ran into

Inspiration QR codes make everyday tasks fast, but they also hide the destination until it is too late. We were inspired to build a simple safety layer that helps users check a QR code before opening a potentially harmful link, payment page, or phishing website. What it does ScanSure scans a QR code through the camera, uploaded image, or pasted link and reveals the full destination before it opens. It assigns a safety score and explains suspicious signals such as insecure HTTP links, IP-address destinations, risky domain endings, phishing keywords, unusually long URLs, and look-alike brand domains. For higher-risk links, it shows an emergency reminder not to share OTPs, passwords, UPI PINs, or payment details. It also includes a safe domain preview, multi-language safety explanations, local scan history, and local scam-report counts. How we built it We built ScanSure as a responsive web application using HTML, CSS, and JavaScript. Camera access uses the browser MediaDevices API. QR decoding uses the BarcodeDetector API where available, with a jsQR fallback for broader browser support. The risk engine analyzes the decoded URL locally using rule-based checks. Scan history and demo reports are stored in the browser using local storage, keeping user data on the device. We also created safe, reserved-domain QR test cases to test suspicious-link detection without pointing users to real scam pages. Challenges we ran into The biggest challenge was browser support for live camera scanning. Different phones and browsers expose different camera features, and not all support BarcodeDetector or hardware zoom. We solved this by adding a QR-decoding fallback, rear-camera fallback, software zoom, and clearer camera-permission guidance. Another challenge was balancing strong scam warnings with privacy. We chose local analysis and local history for the prototype, rather than sending scanned links to a server.

### Accomplishments we're proud of

We are proud that ScanSure makes an invisible QR destination visible before a user clicks it. The project combines practical QR scanning with understandable scam warnings, look-alike domain detection, emergency payment guidance, and an accessible interface.

### What we learned

We learned about browser camera permissions, QR decoding across different devices, URL-security heuristics, and the limitations of web-based camera controls. Most importantly, we learned that security tools need to explain risk in simple language, not just show a technical warning.

### What's next

for Fake QR Code Detector Next, we want to add a real community-reporting backend, threat-intelligence APIs, better domain-age and reputation checks, multilingual voice warnings, and an Android app. We also want to improve the AI explanation system so users receive personalized, easy-to-understand guidance before opening risky links. Accomplishments that we're proud of What we learned What's next for fake qr code detector

## README (from the GitHub repository)

No README available.

## Detected evidence (automated analysis)

Indexed codebase: 4 recognized source files, 35 KB.
- CSS (language) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- Python (language) — detected in the code

## Codebase structure (from repository index)

### Files (5 of 5)

```
.gitignore
app.js
generate_test_qrs.py
index.html
style.css
```

### Dependencies

No dependency index available.

### Recent commits (newest first)

- Initial commit: ScanSure QR Safety Check web app

## Key source files (fetched from GitHub, selected and truncated for size)

### app.js

```javascript
const $ = (s) => document.querySelector(s);
const stage = $('#scanStage'), video = $('#video'), result = $('#resultCard');
let stream, scannedLink = '', zoomTrack, zoomMin = 1, zoomMax = 3, zoomValue = 1, digitalZoom = 1;
let lightTimer, currentRisk = 0, currentFlags = [], currentHost = '';
let adaptiveZoomEnabled = true, autoZoomSweepStep = 0, lastAdaptiveStepTime = 0;
const sweepLevels = [1.0, 1.4, 1.8, 2.2];

const scanCanvas = document.createElement('canvas');
const scanCtx = scanCanvas.getContext('2d', { willReadFrequently: true });

function normalise(value) {
  value = value.trim();
  if (!/^https?:\/\//i.test(value)) value = 'https://' + value;
  try { return new URL(value); } catch { return null; }
}

function inspect(value) {
  const url = normalise(value);
  if (!url) { $('#cameraNote').textContent = 'That does not look like a valid web address.'; return; }
  scannedLink = url.href;
  const host = url.hostname.toLowerCase();
  const flags = [];

  const trustedBrands = ['paytm','phonepe','googlepay','gpay','amazon','flipkart','microsoft','apple','whatsapp','instagram','facebook','netflix'];

  // 1. RFC 2606 & Dummy / Example / Mock Domains
  const dummyDomains = ['example.com', 'example.org', 'example.net', 'example.edu', 'test.com', 'fake.com', 'dummy.com', 'mock.com', 'sample.com', 'localhost', 'invalid'];
  const isDummy = dummyDomains.some(d => host === d || host.endsWith('.' + d));
  if (isDummy) {
    flags.push(`This link uses an example or dummy domain (${host}) which does not belong to a real live service.`);
  }

  // 2. Free app hosting & tunnel platforms
  const freeHosting = ['ngrok.io', 'ngrok-free.app', '000webhostapp.com', 'glitch.me', 'repl.co', 'loca.lt', 'trycloudflare.com', 'weebly.com', 'wixsite.com', 'pages.dev', 'workers.dev', 'vercel.app', 'netlify.app', 'surge.sh', 'github.io', 'herokuapp.com'];
  const isFreeHost = freeHosting.some(h => host === h || host.endsWith('.' + h));
  if (isFreeHost) {
    flags.push('This site is hosted on a free/temporary app hosting platform commonly used for unverified staging or phishing links.');
  }

  // 3. Shortened URLs masking destination
  const shorteners = ['bit.ly', 'tinyurl.com', 't.co', 'is.gd', 'cutt.ly', 'rb.gy', 'shorturl.at', 'ow.ly', 'buff.ly', 'goo.gl'];
  const isShortener = shorteners.some(s => host === s || host.endsWith('.' + s));
  if (isShortener) {
    flags.push('This is a shortened link which hides its true destination address.');
  }

  // 4. Generic Service Subdomains on unverified domains
  const serviceSubdomains = /^(app|services?|portal|auth|secure|login|pay|banking|account|update|support|verify)\./i;
  if (serviceSubdomains.test(host) && !isDummy && !trustedBrands.some(b => host.endsWith(`.${b}.com`) || host.endsWith(`.${b}.in`))) {
    flags.push('The link uses a generic service subdomain (e.g. app., services.) on an unverified domain.');
  }

  // 5. Phishing & Scam Keywords
  const suspiciousWords = /login|verify|secure|wallet|gift|prize|bonus|urgent|support|account|free|services|service|auth|portal|update|pay|payment|claim|reward|upi|bill|recharge|customer|helpdesk|verification|passcode|pin|reset|kyc|refund/i;
  if (suspiciousWords.test(host + url.pathname + url.search)) {
    flags.push('The address contains words commonly used in credential phishing or fake service portals.');
  }

  // 6. Suspicious TLDs
  const suspiciousTld = /\.(xyz|top|click|gq|tk|ml|cf|test|example|invalid|shop|site|online|vip|club|work|tech|link|live|info|icu|monster)$/i;
  if (suspiciousTld.test(host)) {
    flags.push('This domain ending is frequently used in short-lived scam campaigns.');
  }

  // 7. IP Address Host
  const hasIp = /^(\d{1,3}\.){3}\d{1,3}$/.test(host);
  if (hasIp) flags.push('The destination uses a raw IP address instead of a domain name.');

  // 8. Lookalike & Brand Mention
  if (host.includes('xn--')) flags.push('This domain may be using look-alike characters (Punycode).');
  const brand = trustedBrands.find(name => host.includes(name));
  if (brand && host !== `${brand}.com` && !host.endsWith(`.${brand}.com`) && !host.endsWith(`.${brand}.in`)) {
    flags.push(`This domain mentions ${brand} but is not an official ${brand} domain.`);
  }

  // 9. Protocol Check
  if (url.protocol !== 'https:') flags.push('This site does not use an encrypted HTTPS connection.');
  if (url.href.length > 105) flags.push('The destination address is unusually long, which can hide its real purpose.');

  const risk = Math.min(96, flags.length * 24 + (url.protocol !== 'https:' ? 12 : 0));
  currentRisk = risk; currentFlags = flags; currentHost = host;
  const safe = risk < 25;
  $('#verdictTitle').textContent = safe ? 'This link looks safe' : risk < 55 ? 'Proceed with caution' : 'This link looks suspicious';
  $('#verdictLabel').textContent = safe ? 'Safety check complete' : 'Potential phishing warning';
  $('#verdictIcon').textContent = safe ? '✓' : '!';
  $('#verdictIcon').style.color = safe ? 'var(--lime)' : 'var(--coral)';
  $('#verdictIcon').style.background = safe ? 'rgba(187,242,70,.15)' : 'rgba(255,129,120,.15)';
  $('#score').innerHTML = `${100 - risk} <small>/ 100</small>`;
  $('#score').style.color = safe ? 'var(--lime)' : 'var(--coral)';
  $('#destinationText').textContent = url.href;
  const checks = flags.length ? flags : ['Uses an encrypted HTTPS connection.', 'No common phishing patterns were found in this address.', 'The destination was revealed before opening.'];
  $('#checks').innerHTML = checks.map((c, i) => `<li class="${flags.length ? 'warn' : ''}">${c}</li>`).join('');
  $('#emergency').classList.toggle('hidden', risk < 45);
  updateExplainer(); updatePreview(); updateLookalike(brand); updateReportCount(); saveHistory(url.href, risk);
  result.classList.remove('hidden'); result.scrollIntoView({behavior:'smooth', block:'nearest'});
}

let barcodeDetectorInstance = null;
async function tryBarcodeDetector(source) {
  if (!('BarcodeDetector' in window)) return null;
  try {
    if (!barc
[truncated — 13718 more characters]
```

### generate_test_qrs.py

```python
from pathlib import Path
import qrcode

cases = {
    "01-safe-example.png": "https://example.com",
    "02-payment-phishing.png": "http://paytm-secure.test/verify-payment?ref=DEMO2026",
    "03-lookalike-domain.png": "https://gpay-bonus.test/claim",
    "04-ip-address-link.png": "http://192.0.2.45/login",
    "05-long-redirect.png": "https://secure-wallet.test/verify/account/urgent/claim?token=DEMO-ONLY-DO-NOT-USE-REAL-DATA",
}

output = Path("test-qr-codes")
output.mkdir(exist_ok=True)

for filename, data in cases.items():
    qr = qrcode.QRCode(version=None, error_correction=qrcode.constants.ERROR_CORRECT_M, box_size=10, border=4)
    qr.add_data(data)
    qr.make(fit=True)
    qr.make_image(fill_color="#111827", back_color="white").save(output / filename)
    print(f"{filename}: {data}")

```

### style.css

```css
:root{--ink:#eef2f7;--muted:#94a1b5;--line:rgba(198,211,231,.13);--navy:#0a1020;--panel:#10192b;--lime:#bbf246;--coral:#ff8178}*{box-sizing:border-box}body{margin:0;background:radial-gradient(ellipse 65% 45% at 50% -10%,#1e3155 0%,transparent 70%),var(--navy);color:var(--ink);font-family:Manrope,sans-serif;min-height:100vh}.shell{width:min(920px,calc(100% - 40px));margin:auto;padding:26px 0 48px}nav{display:flex;align-items:center;justify-content:space-between}.brand{font-weight:800;font-size:18px;color:var(--ink);text-decoration:none;letter-spacing:-.8px;display:flex;gap:9px;align-items:center}.brand-mark{width:28px;height:28px;display:grid;place-items:center;background:var(--lime);color:#0d1623;border-radius:8px;font-size:15px}.nav-status{font:12px 'DM Mono';color:#b8c5d8}.nav-status i{display:inline-block;width:7px;height:7px;background:var(--lime);border-radius:50%;margin-right:7px;box-shadow:0 0 10px var(--lime)}.hero{text-align:center;margin:74px auto 40px;max-width:650px}.eyebrow,.section-label{font:500 11px 'DM Mono';letter-spacing:1.3px;text-transform:uppercase;color:#a8b6ca}.eyebrow span{color:var(--lime);font-size:14px;margin-right:5px}h1{font-size:clamp(42px,7vw,70px);line-height:1.03;letter-spacing:-4px;margin:17px 0 19px;font-weight:800}h1 em{font-style:normal;color:var(--lime)}.hero p{color:var(--muted);font-size:16px;line-height:1.7;margin:auto;max-width:570px}.scanner-card,.result-card{background:linear-gradient(145deg,rgba(27,41,68,.88),rgba(14,23,40,.96));border:1px solid var(--line);box-shadow:0 28px 60px rgba(0,0,0,.2);border-radius:19px;padding:25px}.scanner-top,.verdict-row{display:flex;justify-content:space-between;align-items:center}.scanner-top h2,.verdict-row h2{font-size:19px;letter-spacing:-.7px;margin:5px 0 0}.privacy{font-size:11px;color:#9cabc0;background:rgba(255,255,255,.04);padding:7px 9px;border-radius:7px}.privacy span{color:var(--lime);margin-right:4px}.scan-stage{height:264px;border:1px dashed rgba(178,202,234,.2);border-radius:12px;margin:23px 0 15px;position:relative;overflow:hidden;background:rgba(2,7,17,.24);display:grid;place-items:center}.placeholder{text-align:center;display:grid;gap:8px;color:#dbe4f1}.placeholder small{color:#8090a7;font-size:12px}.scan-icon{height:66px;width:66px;margin:auto;position:relative;border:1px solid rgba(187,242,70,.2);background:rgba(187,242,70,.04);border-radius:14px}.scan-icon span{position:absolute;width:15px;height:15px;border-color:var(--lime);border-style:solid}.scan-icon span:nth-child(1){top:11px;left:11px;border-width:2px 0 0 2px}.scan-icon span:nth-child(2){top:11px;right:11px;border-width:2px 2px 0 0}.scan-icon span:nth-child(3){bottom:11px;left:11px;border-width:0 0 2px 2px}.scan-icon span:nth-child(4){bottom:11px;right:11px;border-width:0 2px 2px 0}.scan-icon b{position:absolute;left:0;right:0;top:21px;font-size:25px;color:var(--lime)}video{position:absolute;width:100%;height:100%;object-fit:cover;display:none}.camera-on video{display:block}.camera-on .placeholder{display:none}.scan-frame{display:none;position:absolute;width:140px;height:140px;border:2px solid var(--lime);border-radius:9px;box-shadow:0 0 0 1000px rgba(0,0,0,.18)}.camera-on .scan-frame{display:block}.controls{display:flex;gap:10px}.button{border:0;border-radius:9px;padding:12px 17px;cursor:pointer;font:700 13px Manrope;transition:.18s;display:inline-flex;gap:8px;justify-content:center;align-items:center}.button:hover{transform:translateY(-1px);filter:brightness(1.06)}.primary{background:var(--lime);color:#101a25}.secondary{color:#d5dfec;background:#1a2740;border:1px solid rgba(255,255,255,.09)}.camera-note{text-align:center;color:#77869d;font-size:11px;margin:14px 0 0}.manual-row{border-top:1px solid var(--line);margin-top:22px;padding-top:18px;display:flex;align-items:center;gap:9px;color:#8796ab;font-size:12px}.manual-row input{flex:1;background:#0b1323;border:1px solid #263650;color:#dbe4f1;outline:0;border-radius:7px;padding:10px 11px;font:12px 'DM Mono'}.manual-row input:focus{border-color:#769742}.manual-row button{padding:10px 13px;border:0;background:transparent;color:var(--lime);font:700 12px Manrope;cursor:pointer}.result-card{margin-top:20px}.hidden{display:none}.verdict-icon{width:42px;height:42px;border-radius:50%;display:grid;place-items:center;background:rgba(187,242,70,.15);color:var(--lime);font-weight:800;font-size:20px;margin-right:13px}.verdict-row>div:nth-child(2){margin-right:auto}.score{font:700 23px 'DM Mono';color:var(--lime)}.score small{font:11px 'DM Mono';color:#8796aa}.destination{margin:23px 0 14px;background:#0a1221;border:1px solid rgba(255,255,255,.06);padding:13px;border-radius:9px;display:grid;grid-template-columns:auto 1fr auto;gap:10px;align-items:center}.destination span{font:11px 'DM Mono';color:#8392a7;text-transform:uppercase}.destination code{font:12px 'DM Mono';color:#dce8f5;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.destination button{background:transparent;border:0;color:#a7b8ce;font-size:17px;cursor:pointer}.checks{list-style:none;padding:0;margin:0;display:grid;gap:8px}.checks li{font-size:12px;color:#afbdce}.checks li:before{content:'✓';color:var(--lime);font-weight:800;margin-right:9px}.checks li.warn:before{content:'!';background:rgba(255,129,120,.16);border-radius:50%;display:inline-grid;place-items:center;color:var(--coral);width:16px;height:16px;margin-right:9px}.checks li.warn{color:#ffd0cc}.result-actions{display:flex;justify-content:flex-end;gap:9px;border-top:1px solid var(--line);padding-top:18px;margin-top:20px}.how-it-works{display:grid;grid-template-columns:repeat(3,1fr);gap:30px;margin:52px 13px 0}.step{font:500 11px 'DM Mono';color:var(--lime)}h3{font-size:15px;margin:8px 0 5px}.how-it-works p{font-size:12px;line-height:1.55;color:#8998ad;margin:0}.toast{position:fixed;bottom:25px;left:50%;translate:-50% 80px;background:#e6f2d0;color:#16220a;padding:10px 15px;border-radius:8px;font-size:12px;font-weight:700;transition:.3s}.toast.show{translate:-50% 0}@media
[truncated — 399 more characters]
```

### index.html

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta name="theme-color" content="#0a1020" />
    <title>ScanSure — QR Safety Check</title>
    <link rel="preconnect" href="https://fonts.googleapis.com" />
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
    <link href="https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Manrope:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
    <link rel="stylesheet" href="style.css" />
    <style>.zoom-control{display:flex;align-items:center;justify-content:center;gap:9px;margin:-5px 0 15px;color:#aebdd0;font:600 14px Manrope;flex-wrap:wrap}.zoom-control input{width:135px;accent-color:#bbf246;cursor:pointer}.zoom-control button{width:25px;height:25px;border:1px solid #31415d;border-radius:50%;background:#17233a;color:#dce8f5;cursor:pointer;font-size:17px;line-height:1}.zoom-control .adaptive-btn{width:auto;height:26px;padding:0 10px;border-radius:13px;font:700 11px Manrope;border:1px solid #31415d;background:#17233a;color:#9aa9bd;cursor:pointer;transition:all 0.2s}.zoom-control .adaptive-btn.active{background:rgba(187,242,70,.18);color:#bbf246;border-color:rgba(187,242,70,.5)}.intel-grid{display:grid;grid-template-columns:1fr 1fr;gap:11px;margin-top:15px}.intel-card{border:1px solid rgba(255,255,255,.08);background:#0b1323;border-radius:10px;padding:13px}.intel-card h3{margin:0 0 7px;font-size:12px;color:#dce8f5}.intel-card p{margin:0;color:#9aa9bd;font-size:12px;line-height:1.5}.intel-card select,.intel-card button{margin-top:10px;background:#18263f;border:1px solid #30405b;border-radius:6px;padding:7px;color:#e2ebf5;font:600 11px Manrope;cursor:pointer}.emergency{margin-top:15px;padding:13px;border:1px solid rgba(255,129,120,.42);border-radius:10px;background:rgba(255,129,120,.09);color:#ffd4d0;font-size:12px;line-height:1.55}.emergency strong{color:#ff9b94;display:block;margin-bottom:3px}.history{margin-top:26px}.history-head{display:flex;justify-content:space-between;align-items:center}.history-head h2{font-size:16px;margin:0}.history-head button{border:0;background:transparent;color:#9fafc2;font:600 11px Manrope;cursor:pointer}.history-list{margin:10px 0 0;padding:0;list-style:none}.history-list li{display:flex;justify-content:space-between;gap:10px;padding:9px 0;border-bottom:1px solid rgba(255,255,255,.07);font:11px 'DM Mono';color:#c7d3e1}.history-list small{color:#8090a5;white-space:nowrap}.community-count{color:#bbf246;font-family:'DM Mono';font-size:11px}@media(max-width:600px){.intel-grid{grid-template-columns:1fr}}</style>
  </head>
  <body>
    <main class="shell">
      <nav>
        <a class="brand" href="#"><span class="brand-mark">S</span> ScanSure</a>
        <span class="nav-status"><i></i> Protection active</span>
      </nav>

      <section class="hero">
        <div class="eyebrow"><span>✦</span> QR code safety, before you click</div>
        <h1>Know where a<br /><em>QR code</em> leads.</h1>
        <p>Scan any QR code and we’ll show the destination, check it for phishing signals, and help you decide if it’s safe to open.</p>
      </section>

      <section class="scanner-card" aria-label="QR code scanner">
        <div class="scanner-top">
          <div>
            <span class="section-label">Scanner</span>
            <h2>Check a QR code</h2>
          </div>
          <span class="privacy"><span>◉</span> Nothing is opened automatically</span>
        </div>

        <div class="scan-stage" id="scanStage" tabindex="0" role="button" aria-label="Drop a QR image here or press Enter to choose an image">
          <video id="video" playsinline muted></video>
          <div class="placeholder" id="placeholder">
            <div class="scan-icon"><span></span><span></span><span></span><span></span><b>⌁</b></div>
            <strong>Ready to scan</strong>
            <small>Point your camera at any QR code to scan</small>
          </div>
          <div class="scan-frame" aria-hidden="true"></div>
        </div>
        <div class="zoom-control hidden" id="zoomControl">
          <button id="zoomOut" type="button" aria-label="Zoom out">−</button><input id="zoomSlider" type="range" min="1" max="3" value="1" step="0.1" aria-label="Camera zoom" /><button id="zoomIn" type="button" aria-label="Zoom in">+</button>
          <button id="adaptiveZoomBtn" class="adaptive-btn active" type="button" title="Toggle Adaptive Auto-Zoom">✦ Auto Zoom</button>
        </div>

        <div class="controls">
          <button class="button primary" id="cameraBtn"><span>⌁</span> Scan with camera</button>
          <label class="button secondary" for="fileInput"><span>↥</span> Upload QR image</label>
          <input id="fileInput" type="file" accept="image/*" hidden />
        </div>
        <p class="camera-note" id="cameraNote">Drop an image in the scan box or use your camera. Your files stay private on this device.</p>

        <div class="manual-row">
          <span>Or paste a link</span>
          <input id="manualUrl" type="text" placeholder="Paste a URL to check safety" aria-label="Paste a link" />
          <button id="checkBtn">Check link</button>
        </div>
      </section>

      <section class="result-card hidden" id="resultCard" aria-live="polite">
        <div class="verdict-row">
          <div class="verdict-icon" id="verdictIcon">✓</div>
          <div><span class="section-label" id="verdictLabel">Safety check complete</span><h2 id="verdictTitle">This link looks safe</h2></div>
          <span class="score" id="score">92 <small>/ 100</small></span>
        </div>
        <div class="destination">
          <span>Destination</span>
          <code id="destinationText"></code>
          <button id="copyBtn" title="Copy destination">⧉</button>
        </div>
        <ul class="checks" id="checks"></ul>
        <div class="emergency hidden" id="emergency"><strong>Stop before you
[truncated — 1980 more characters]
```