SECURITY & TRUST

How your compliance data is protected

This page is for a broker's compliance officer or auditor deciding whether to switch KYTGate on. It covers where the data is, who can reach it, how tenants are kept apart, and what is cryptographically signed as opposed to only logged. It also lists what we have not done yet. Send any questions to hello@kytgate.com.

On this page

Where data lives

ApplicationVercel (serverless functions, TLS 1.2+ everywhere, no long-lived servers to patch).
DatabaseNeon managed Postgres, region US East (AWS us-east-1). Encrypted at rest and in transit; point-in-time restore per the Neon plan. One database, tenant isolation by row-level tenant id on every table.
E-mailResend, for transactional mail only: wallet-watch confirmations and change alerts, sandbox claim links, contact-form and sandbox-request notifications to our own inbox, and customer information requests sent on a tenant's instruction. No marketing mail exists.
Blockchain readsPublic JSON-RPC endpoints per network, Blockscout public explorer API instances (eth, base, arbitrum, polygon, optimism.blockscout.com) for wallet profiles and funds traces, and TronGrid for Tron. A read contains the address or transaction being checked, and nothing about the tenant or its customer.
Cached public dataAnswers from the public explorers are cached so that tracing the same well-known address twice does not query a public service twice. A contract's public label is kept for up to 7 days. On the free public tools only, an unlabelled wallet's public transaction profile is kept for 15 minutes. A cache key is a chain, an address and the explorer it came from, so no tenant identifier can enter one. Screenings never read this cache. Every input to a decision, including the optional wallet-behaviour observation, is read live at the moment of the screening. We did cache the observation for a while and then stopped. A deposit address receiving its second deposit is the very event that changes what the observation says, so the cached answer altered the evidence instead of speeding it up.
Read live, every screeningSanctions matches, USDT/USDC issuer freezes, the Chainalysis oracle, transaction verification and the wallet-behaviour observation. A cached 'not frozen' could be wrong with a customer's money at stake, and a stale observation would put an incorrect statement in a case file.
Sanctions listsRe-synced at least every 6 hours from OFAC, the UK Sanctions List and the EU; stored as content-hashed snapshots so any decision can name the exact list version it used.
SecretsVercel encrypted environment variables (sensitive mode). The receipt signing key never leaves the runtime; its public half is published at /api/v1/receipt-key.
Subprocessors, in fullVercel Inc. (application hosting and server logs, USA / EEA); Neon Inc. (managed postgres database, USA (AWS us-east-1)); Resend Inc. (transactional e-mail, USA); Functional Software, Inc. (Sentry) (error reporting, EEA (Germany)); TRON DAO (TronGrid) (tron network reads, Public network endpoint); Blockscout public explorer API instances (wallet profile and funds trace reads on evm networks, Public endpoints (eth, base, arbitrum, polygon, optimism.blockscout.com)); Public blockchain JSON-RPC providers (on-chain reads per network, Public endpoints, per network, configurable per deployment). What each one receives, and the date it began processing, is in clause 7 of the DPA. We give at least 30 days’ notice before a new one starts, and the dates in that table let you check that we did.
Tenant isolationEvery query is scoped by the API key's tenant, and a live isolation audit tries to cross tenants on every endpoint.

Every table carries a tenant id, and every query is scoped by the API key that made the request. The key resolves to exactly one tenant and cannot be pointed elsewhere. Idempotency keys, processor signals, wallets, cases, reports and replays are all tenant-scoped. We test this instead of relying on code review. The console runs an isolation audit against the live API with two sandbox tenants, one of them seeded. Every endpoint is called with the second tenant's key against the first tenant's identifiers, and there are no-key and garbage-key probes as well. The latest run is recorded in the audit log and shown in the console. Any customer can ask for its output.

Access control

APIBearer keys, stored as SHA-256 hashes (the plaintext is shown once at creation). Revocation is immediate; keys never expire silently.
ConsoleNamed user accounts with scrypt-hashed passwords (N=65536, r=8, p=2, parameters stored with each hash and raised automatically at the next sign-in) and 12-hour sessions in a single httpOnly cookie. The admin and reviewer roles are enforced on every page and every action. Login throttling is counted in the database over a 15-minute window: 10 failures per e-mail-and-IP pair, 20 per account regardless of IP (so a rotating-IP attacker still hits a ceiling), 30 per IP. A forgotten password is reset from a signed e-mail link that is valid for 30 minutes and works once. On arrival the token is moved out of the address into an httpOnly cookie, so it does not stay in browser history or page logs. The link stops working as soon as the password changes, and the reset signs out every other session of that account. The form gives the same answer whether or not the address exists, and is limited to 3 links per address and 10 per IP an hour. Logins are tenant-scoped. A customer's admin and reviewer accounts see and act on that tenant only. Every page, list, action and console API call is filtered server-side, and another tenant's record returns 404. KYTGate staff accounts are global. They see every tenant and act only on a customer's documented instruction. Every action records the acting user.
Four-eyesA BLOCK_PENDING_MLRO case cannot be closed by one person. The proposer and the approver must be different accounts, and the approver cannot alter the proposed outcome.
Break-glassAn operator secret exists for emergencies. It is exchanged on first use for a one-hour httpOnly grant and removed from the address, logging out revokes the grant on the server, and every use is written to the audit log as actor 'break-glass'.
Your provider keysIf you connect your own intelligence licence, the key is sealed with AES-256-GCM before it is written, bound to your tenant so a sealed value copied elsewhere does not open. The sealing key material is held in the environment and never in the database, so a leaked database exposes only ciphertext. No endpoint reads a key back. The console shows a four-character tail and a hash, which is enough for you to recognise the key you pasted and useless to anyone else. A deployment with no sealing key configured refuses to accept a provider key rather than store it in the clear. Disconnecting deletes the key rather than disabling it. If you connect a provider by describing its endpoint yourself, that description is stored as ordinary settings and holds no credential. You write {key} where the credential belongs, and KYTGate substitutes the sealed value at call time. A description that contains the credential itself is refused on entry, at the point where we can still recognise it as a credential. Commercially, the call is made under your contract and billed to you, so your provider is your sub-processor rather than ours. KYTGate sees their data only as the answer it records in your own case file.
Audit logAppend-only. Every console mutation, mode switch, key creation, policy activation, override, export, retention purge and isolation run: who, how, from where (the network, /24 or /48, and a keyed hash of the acting user's IP address; never the address itself), on what.
Integrity and evidenceEd25519-signed receipts anyone can verify, a published daily anchor that dates them, four things a tenant policy is refused for, HMAC-signed webhooks.

Every screening produces a Decision Receipt signed with Ed25519 over a canonical JSON of its inputs, list snapshot hashes, coverage, signals, policy version and decision. Anyone can verify a receipt in their browser at /tools/receipt-verifier without an account and without trusting us. For an adversarial audit it is better to verify on your own machine, with no page of ours involved: npx kytgate-verify receipt.json. This is an MIT-licensed package with no dependencies whose published files are its source. There is no build step, so the code an auditor runs is the code they can read. Policies are immutable versions. Built-in baselines are frozen by version, so a decision from March can be replayed under March's rules in December. Webhooks are HMAC-SHA256 signed with a per-tenant secret and a timestamp.

A signature proves KYTGate said something. On its own it cannot prove when, because the signing key is ours. So once a day every receipt written so far is hashed into an RFC 6962 Merkle tree and the root is published at /attestations and never rewritten. A number published before a deposit was taken cannot be recomputed later to cover a receipt written after it, and the holder of a receipt can fold their own inclusion proof back to that published root without asking us for anything. The claim is narrow, and the attestations page says so. The anchor proves a receipt existed, in the form you hold, before the root was published, and that it has not changed since. It does not prove the decision was right, and it says nothing about receipts that are not in the batch. A root we publish ourselves is a tamper-evident log, not third-party proof, which is why the page asks an auditor to record the day's root somewhere we do not control.

Examiners ask the same question about every configurable control: what stops the customer turning it off? Tenants write their own policies and can make them as strict as they like. Four things are refused outright. The check runs the real decision engine against synthetic inputs before the policy can be stored, instead of reading the rules. A policy that would ALLOW a direct sanctions match on full coverage is refused. A policy that would ALLOW with nothing adverse found but processor evidence MISSING is refused, because free public sources alone never prove “clean”. A default decision of ALLOW is refused, so an unmatched case can never pass silently. And a behaviour rule that would act at or below the wallet observation's severity is refused, so the signal we publish as informational cannot be quietly promoted into a blocker. Activating a new version is a single statement, so there is no moment at which a tenant has no active policy and silently falls back to our baseline.

The anchor conflicts with erasure in one place, and we resolved it in favour of the other customers. When a tenant leaves, the pointers from its receipts into the Merkle leaves are removed, but the leaf hashes stay. Deleting one would break the published root for every other receipt in that day's batch, so one tenant's retention schedule would destroy another's evidence. A leaf hash on its own identifies nobody. It is the hash of a receipt whose contents have been deleted.

Data minimisationNo cookies or analytics on the public site; no permanent record of free-tool queries; tenant data deleted on its retention schedule.

The public site sets no cookies and runs no analytics. The console sets one httpOnly session cookie. Free tool queries leave no permanent record, and nothing links a query to the person who made it. What is kept is a short-lived rate-limit counter keyed to a pseudonymised IP (truncated SHA-256) and purged after 7 days. Separately, there is the cache of public chain data described above, which is keyed by chain and address and holds no identifier of the person asking. Sandbox requests store an e-mail, an optional company name and the same truncated IP hash, and a copy goes to our own inbox. Sandbox tenant data is deleted after 7 days. Wallet watch stores an address and an e-mail only after a confirmation click, and deletes the record 30 days after the person stops it. Tenant data is deleted on the tenant's retention schedule (default 7 years, legal hold available) and 30 days after a tenant closes. A full JSON export is available before that. See the privacy policy.

Availability and incidentsFail-closed (TECHNICAL_HOLD, never ALLOW); lists re-synced every 6 h; incidents notified within 48 hours of confirmation.

KYTGate fails closed. If a mandatory source (sanctions snapshot older than the tenant's limit, transaction verification) cannot answer, the decision is TECHNICAL_HOLD, never ALLOW. Sanctions lists are re-synced at least every 6 hours (the live figure is on the status page). Every scheduled job reports each step, and the console shows a red banner when lists are stale. Webhook deliveries are retried every 10 minutes with backoff. Security incidents affecting a tenant's data are notified to that tenant without undue delay and within 48 hours of confirmation, with what happened, what data, and what we did.

We spend the most effort on silent failures. A job that reports its own errors cannot report that it has stopped running, and a retired list usually keeps downloading. Three checks therefore look for silence rather than errors. A source that still fetches but whose content has not changed in a long time raises an alert. A sanctions list that its publisher had closed once sat here looking healthy in exactly this way. A separate watchdog checks that the scheduled jobs have run at all and that the database has every table the deployed code expects. Finally, we count failures of the providers that are allowed to fail. They are non-mandatory so that a slow third party never holds a deposit. If one of them fails on more than half of a day's screenings, that raises an alert too, because otherwise we would still be claiming a check that had quietly stopped working. None of this can detect our own host going down, because nothing would be left running to raise the alarm. For that, /api/health is there to be polled from outside. It returns 503 rather than 200 when the product cannot do its job.

If KYTGate disappearsYour export verifies with no connection to us: the public key, the daily Merkle roots and one inclusion proof per receipt travel with it.

A compliance officer needs to know whether the evidence still holds up if a vendor goes away for good. A decision you have to defend in three years was made on our platform, and by then we may not exist.

Everything needed to check a receipt is inside the export. Until recently the export said receipts were “verifiable at our verifier tool with the public key from our key endpoint”. Both of those are our URLs, so the export could only be checked while we were still answering, which is when nobody needs to check it. The export now carries the Ed25519 public key, the Merkle root of every batch your receipts sit in, and one inclusion proof per receipt. npx kytgate-verify is a single dependency-free file you can read end to end, and with those three it makes no network call at all.

Content and date. The signature proves a receipt is byte-for-byte what we produced. It cannot prove the receipt existed on the date it claims. We hold the signing key, so nothing would stop us signing something later and dating it earlier. The Merkle root and the inclusion proof answer that. Until now the proof existed only inside our database, so nobody holding an export could check the anchor. Now they can. Where a batch carries an RFC 3161 timestamp, the date does not rest on our word either.

What the export leaves out. The export lists every table it carries, every table it leaves out and why, and every receipt it could not produce a proof for, with the reason. A gap in that list is a defect in our export rather than data you do not have. Before the list existed, two tables had silently dropped out of the export.

Two things are not solved. The file bytes of documents your customers uploaded are downloaded separately rather than inlined. And the roots are worth most if you recorded them from /attestations while you were a customer, rather than taking ours at the moment you need the answer. The export itself states both.

What we have not done yetNo SOC 2 / ISO 27001, no third-party pentest yet, no SSO/SAML, single-region database, no rehearsed database restore and no claimed recovery time.

No SOC 2 or ISO 27001 certification. No third-party penetration test yet (planned before commercial launch; the isolation audit is our own). No SSO/SAML (planned for the enterprise tier). Single-region database.

No rehearsed database restore, and no recovery time. Point-in-time restore comes with the database plan. We have not performed one, so we claim no number: no RTO, no RPO, no "typically minutes". Treat restore as untested until we have rehearsed one and can tell you how long it took. What has been rehearsed is the other half of the question, the half that protects you if KYTGate itself does not come back. See if KYTGate disappears.

A root's independent timestamp is a Bitcoin anchor, not a qualified one. Every day's Merkle root is submitted to four independent OpenTimestamps calendar servers, which aggregate it into a Bitcoin transaction; the proof is a path from the root to a block header, checkable with the reference client and your own node, trusting neither us nor any certificate authority. There are two limits. A proof takes an hour or two to confirm, and until it does that batch's row reads “not yet in a block” and evidences nothing about time. And a Bitcoin anchor carries no legal presumption of time under eIDAS, which only an accredited trust service can give; we do not ask one, because no Time-Stamping Authority publishes terms permitting a commercial product to use it for free, and we would rather tell you that than rely on a service nobody granted us. If your auditor requires a qualified timestamp, tell us and we will contract one. The code asks any RFC 3161 authority you name alongside the calendars. /attestations shows the state per batch, and the proof files are downloadable there.

Reporting a vulnerabilityhello@kytgate.com, acknowledged in 3 working days, no legal action for research inside this policy, no bug bounty.

If you have found something, send it to hello@kytgate.com. The machine-readable version of this is at /.well-known/security.txt (RFC 9116).

What we will do. We acknowledge a report to hello@kytgate.com within 3 working days, tell you within 10 working days whether we consider it a vulnerability and what we intend to do, and tell you when it is fixed. If you want credit on this page you will get it; if you would rather not be named, say so.

What we will not do. We will not pursue legal action, or ask anyone else to, over research that stays inside this policy — and that holds even if the report turns out to be wrong or is something we already knew. We do not require you to sign anything before reporting, and we will not ask you to keep quiet indefinitely: if we have not fixed something in 90 days, publishing is reasonable.

There is no bug bounty. We do not pay for reports, and saying so plainly is fairer than implying otherwise. If that changes it will be stated here first.

In scope. kytgate.com and its subdomains, the API, the console, and the free public tools. The published receipt verifier and the evidence-pack format are explicitly in scope: if you can make a receipt verify that should not, or read a tenant's data through any endpoint, that is the report we most want.

Out of scope, and please do not. Denial of service, load or stress testing, and anything that degrades the service for other tenants. Social engineering of our people or our suppliers. Physical attacks. Automated scanner output with no demonstrated impact. Reports about a third party's infrastructure — send those to the third party. Never use another tenant's real data to prove a point: tell us how, and we will reproduce it ourselves.

Known already, so not a finding. What we have not done yet is listed on this page, above, and each item there is a known gap rather than a discovery: no SOC 2 or ISO 27001, no third-party penetration test, no SSO, a single-region database. A report that restates one of those is welcome but will be closed as known.

DocumentsDPA template, terms, privacy policy and API documentation.

Try it on your own traffic

Start a sandbox and run it alongside your current process. It keeps the evidence described on this page.

Start free sandboxTalk to usSee pricing

API key in two minutes, 50 screenings a month free, no card. Security · Live status · Methodology