DEVELOPER DOCUMENTATION

Frontend Integration

Add MANDATE bot detection to your frontend with a single script tag.

Start in Observe on one route. Use the deployment URL and installed package version supplied for your workspace. Historical host examples and source capabilities do not establish a live service or published release.

Installation

Warning

Wire-name status: _mndt, x-md-vq, x-md-w / x-md-h, and

md_<seed> purpose cookies are the canonical SDK baseline in this repository,

but are undeployed. Legacy names remain accepted as documented.

Add the MANDATE script to your HTML:

html
<script async src="https://gate.x-lock.dev/g.js?k=YOUR_SITE_KEY"></script>

Script Parameters

ParameterRequiredDescription
k (query param)YesYour site key from the MANDATE dashboard
async (attribute)RecommendedPrevents render blocking

Info

The script is lightweight, loads asynchronously, and does not block rendering. There is no user-facing interaction — verification happens entirely in the background.

Same-Origin Collector Alias

High-value deployments can route collector traffic through the customer app or edge hostname instead of exposing gate collector paths directly in browser network logs. Configure the site key with a dedicated collector alias:

json
{
  "collector": {
    "origin": "https://app.example.com",
    "path_prefix": "/__mndt",
    "proxy_metadata": {
      "mode": "monitor",
      "scope": "customer_alias",
      "kill_switch": false
    }
  }
}

Then proxy the bundle-selected app routes to the matching gate route without rewriting the body. Gate-owned non-alias routes use a daily rotated /rx/<daily>/... prefix by default, while customer alias paths keep your configured stable prefix and proxy to the stable selected gate route:

Customer routeGate route
/__mndt/<seed>/a/r/<seed>/a
/__mndt/<seed>/b/r/<seed>/b
/__mndt/<seed>/c?k=YOUR_SITE_KEY/r/<seed>/c?k=YOUR_SITE_KEY
/__mndt/a/<seed>/rt/a/<seed>
/__mndt/b/<seed>/rt/b/<seed>
/__mndt/c/<seed>?k=YOUR_SITE_KEY/rt/c/<seed>?k=YOUR_SITE_KEY
/__mndt/<seed>/e/a/rx/<seed>/e/a
/__mndt/<seed>/e/b/rx/<seed>/e/b
/__mndt/<seed>/e/c?k=YOUR_SITE_KEY/rx/<seed>/e/c?k=YOUR_SITE_KEY

Keep the prefix dedicated to MANDATE, forward normal request metadata such as User-Agent, Origin, Referer, and X-Forwarded-*, and do not expose proxied collector payloads to page scripts.

For production aliases, sign the proxy metadata so MANDATE can distinguish a real customer proxy from a browser that spoofed X-Mndt-Collector-Path. The proxy should preserve the encrypted request body and add only bounded routing metadata:

javascript
import { createHash, createHmac } from "node:crypto";

function signingSecret(siteSecret) {
  const hash = createHash("sha256").update(siteSecret).digest("hex");
  return Buffer.from(`mandate-token-v1:${hash}`, "utf8");
}

function proxySignature({ method, gatePath, collectorPath, timestamp, proxyId, siteSecret }) {
  const base = ["v1", method.toUpperCase(), gatePath, collectorPath, timestamp, proxyId].join("\n");
  return `sha256=${createHmac("sha256", signingSecret(siteSecret)).update(base).digest("hex")}`;
}

function requestShapeSignature({ method, gatePath, protocol, h2Settings, headerOrder, timestamp, siteSecret }) {
  const base = ["v1", method.toUpperCase(), gatePath, protocol || "", h2Settings || "", headerOrder || "", timestamp].join("\n");
  return `sha256=${createHmac("sha256", signingSecret(siteSecret)).update(base).digest("hex")}`;
}

Forward these headers with the proxied collector request:

HeaderValue
X-Mndt-Collector-PathThe customer route, for example /__mndt/<seed>/a or /__mndt/a/<seed>
X-Mndt-Proxyv1
X-Mndt-Proxy-IDStable deployment label such as edge-us-east
X-Mndt-Proxy-TsUnix timestamp in seconds
X-Mndt-Proxy-SignatureHMAC over v1\nMETHOD\nGATE_PATH\nCUSTOMER_PATH\nTIMESTAMP\nPROXY_ID
X-Mndt-ProtocolOptional trusted edge protocol hint (h2, h3, or http/1.1)
X-Mndt-H2-SettingsOptional bounded HTTP/2 settings-order hint when available
X-Mndt-Header-OrderOptional bounded request-header order hint when available
X-Mndt-Request-Shapev1 when signing request-shape hints
X-Mndt-Request-Shape-TsUnix timestamp in seconds
X-Mndt-Request-Shape-SignatureHMAC over v1\nMETHOD\nGATE_PATH\nPROTOCOL\nH2_SETTINGS\nHEADER_ORDER\nTIMESTAMP

Tip

If you run Cloudflare in front of your app, you do not have to hand-roll this.

The ready-made MANDATE Cloudflare connector

(connectors/cloudflare-worker) maps the /__mndt/* routes to the gate, signs

the proxy and request-shape metadata for you, and can optionally verify X-Mndt

at the edge.

Tip

For the golden Next.js App Router path, use the first-party collector

reference at

connectors/nextjs-collector.

It signs proxy metadata, forwards opaque bodies, and re-appends multi-value

Set-Cookie (including HttpOnly _mndt; legacy Mndt-Sess remains accepted) on the customer origin. Treat it

as a reference recipe — not a published managed connector.

The gate verifies this metadata for dashboard health reporting by default. Customer Integrations and staff Operations show whether recent customer-alias traffic is ready for a stricter requirement. To link enforcement, the site must set collector.proxy_metadata.mode to enforce and MANDATE operations must set MANDATE_PROXY_METADATA_ENFORCEMENT=true; either MANDATE_PROXY_METADATA_KILL_SWITCH=true or site-level collector.proxy_metadata.kill_switch=true returns the deployment to report-only. Once linked, collector verify/delta traffic for that site must carry valid signed proxy metadata, so direct unsigned calls to canonical /v or /v/delta are blocked instead of treated as fallback traffic. MANDATE still decrypts, scores, and enforces policy on the gate; customer proxy code should not inspect collector payloads or make allow/block decisions.

Token Delivery

MANDATE keeps verification tokens private to the browser-verification runtime by default. Public g.js does not create a token meta tag, dispatch a token event, or expose a stable global token getter.

The production frontend contract is first-party credential attachment: after verification through a customer alias, the gate sets the HttpOnly _mndt credential and same-origin protected requests carry it automatically. The legacy Mndt-Sess name remains accepted. This baseline is in-repository and undeployed; the page does not receive raw token material.

The runtime may also attach x-md-vq plus x-md-w/x-md-h. Backend SDKs

forward the latter as work_proof/integrity_proof, and app code does nothing.

After hosted validation, API responses may rotate HttpOnly md_<seed> purpose

cookies through the SDK; app code does nothing.

In non-production header or dual compatibility mode, the client can automatically add the opaque X-Mndt header to same-origin fetch requests and same-origin non-GET/HEAD XMLHttpRequest requests. Same-origin requests made before verification completes are held until the proof is ready.

javascript
await fetch("/api/action", {
  method: "POST",
  body: JSON.stringify({ intent: "submit" }),
});

Production protected routes enforce the cookie credential. Header-mode compatibility backends read and verify:

http
X-Mndt: <opaque-token>

Tip

For cross-origin protected APIs, route browser calls through a same-origin BFF/proxy or serve the protected API from the same origin. Do not scrape page-visible token state from g.js internals.

Legacy Compatibility Bridge

The old page-visible bridge can be restored only outside production for migration shims or trusted diagnostics with MANDATE_TOKEN_BRIDGE=legacy or MANDATE_TOKEN_BRIDGE=diagnostic on the gate. In that mode, g.js publishes meta[name="mndt-token"] and mandate:token events for compatibility.

Warning

The legacy bridge makes the token readable to any script or browser automation running on the page. Keep it disabled for normal production traffic.

CAPTCHA migration shims (/compat/*) require the legacy bridge while they are in use. They never scrape polymorphic g.js internals.

SPA Integration

jsx React
import { useEffect, useState } from "react";

function App() {
  const [ready, setReady] = useState(false);

  useEffect(() => {
    const script = document.createElement("script");
    script.src = "https://gate.x-lock.dev/g.js?k=YOUR_SITE_KEY";
    script.async = true;
    script.onload = () => setReady(true);
    document.head.appendChild(script);

    return () => {
      document.head.removeChild(script);
    };
  }, []);

  async function handleSubmit() {
    await fetch("/api/action", { method: "POST" });
  }

  return <button onClick={handleSubmit} disabled={!ready}>Submit</button>;
}
vue Vue
<script setup>
import { ref, onMounted, onUnmounted } from "vue";

const ready = ref(false);
let scriptEl;

onMounted(() => {
  scriptEl = document.createElement("script");
  scriptEl.src = "https://gate.x-lock.dev/g.js?k=YOUR_SITE_KEY";
  scriptEl.async = true;
  scriptEl.onload = () => (ready.value = true);
  document.head.appendChild(scriptEl);
});

onUnmounted(() => {
  if (scriptEl) document.head.removeChild(scriptEl);
});

async function handleSubmit() {
  await fetch("/api/action", { method: "POST" });
}
</script>

<template>
  <button @click="handleSubmit" :disabled="!ready">Submit</button>
</template>
tsx Next.js (App Router)
"use client";

import Script from "next/script";

export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <>
      <Script
        src="https://gate.x-lock.dev/g.js?k=YOUR_SITE_KEY"
        strategy="afterInteractive"
      />
      {children}
    </>
  );
}

Content Security Policy

If your site uses a Content Security Policy, add the following directives:

Code
script-src 'self' 'wasm-unsafe-eval' https://gate.x-lock.dev;
connect-src 'self' https://gate.x-lock.dev;
worker-src 'self' blob:;

If you use a same-origin collector alias, keep https://gate.x-lock.dev in script-src for /g.js; collector connect-src can stay on 'self' when /__mndt/* is served by the app origin.

Warning

MANDATE verification instantiates WebAssembly, so script-src requires 'wasm-unsafe-eval'. Blob-backed workers also require blob: in worker-src. Do not add 'unsafe-inline' or 'unsafe-eval'; neither is required.

Token Lifecycle

  • Tokens are issued after successful background verification
  • TTL: 15 minutes from issuance
  • The script automatically refreshes tokens before expiry on long-lived pages
  • Tokens are single-use (recommended server-side enforcement)

Troubleshooting

SymptomCauseFix
Missing X-Mndt on protected requestRequest is cross-origin, GET/HEAD XHR, or sent before g.js loadedUse same-origin fetch/mutation XHR and load g.js before protected actions
Token is stale / expiredPage open > 15 min without activityClient auto-refreshes; check CSP connect-src
CSP violationMissing directiveAdd https://gate.x-lock.dev to script-src and connect-src
CSP violation with blockedURI wasm-evalWebAssembly compilation is blockedAdd 'wasm-unsafe-eval' to script-src