Lyntway

Documentation

How to connect it, what a receipt proves, how somebody checks one without us, and what it cannot see.

On this page

  1. Connect it
  2. Deploying it
  3. Two modes
  4. Verify a receipt
  5. What a receipt actually claims
  6. What it cannot see
  7. What we store
  8. Getting hold of your receipts
  9. Tokens, and getting your values back
  10. Run it yourself
  11. Limits worth knowing before you rely on it

In one paragraph. Lyntway watches AI traffic, records what was in it as classes and counts, and signs that record. The receipt verifies against a published key with no account and no network, so your evidence does not depend on us continuing to exist. It never stores your content.

Connect it

1. Change two lines in your environment

The official SDKs read the base URL and the key from the environment, so setting these two governs every call the application makes — including code you did not write and libraries you do not control. No code changes, no new files. Restart and it is live.

OPENAI_BASE_URL=https://YOUR-LYNTWAY/gw/openai/v1
OPENAI_API_KEY=$LYNTWAY_KEY~$YOUR_OPENAI_KEY

The two keys are joined by a tilde because an SDK gives you one key field and no way to add a header. We split them here, forward yours, and keep neither.

Do not paste a snippet into a new file. That governs only the calls inside it while the rest of the application carries on reaching the provider directly — which looks exactly like the product not working.

2. Only if your code sets the URL and key itself

Change it where the client is built, and everywhere it is built.

client = OpenAI(
    base_url="https://YOUR-LYNTWAY/gw/openai",
    api_key=LYNTWAY_KEY,                    # we authenticate on Authorization
    default_headers={"X-Provider-Key": OPENAI_API_KEY},
)

Anthropic is simpler — its own x-api-key passes through untouched, so only the base URL and an Authorization header are needed.

3. Tools with one API key field

Cursor and most editor integrations offer a base URL and a single key box, with no way to add headers. Join the two keys with a tilde and paste that in — we split them, forward yours, and keep neither.

A limit worth knowing before you rely on this. In these editors a base URL applies only to models you add with your own key. Whatever comes bundled with their subscription is routed to their own servers, and nothing you configure here changes that. This covers a team bringing its own provider keys; it does not cover a team on the editor's plan.

Base URL   https://YOUR-LYNTWAY/gw/openai/v1
API key    $LYNTWAY_KEY~$OPENAI_API_KEY

4. MCP servers on a laptop

Most MCP servers run as a subprocess of the agent over a pipe, where no proxy can see them. Point the agent at the shim and it spawns the server itself.

{"mcpServers": {"github": {
  "command": "lyntway-mcp",
  "args": ["--", "npx", "-y", "@modelcontextprotocol/server-github"]
}}}

The shim needs no account. Run on its own it signs with a key it makes at startup and throws away, which is useful for seeing what your own tools handle and worth nothing as evidence to anybody else — the key belongs to the person being audited. Sign in and it signs with a key that does not.

Tool results are governed on the way back, which is the direction that matters: a result carrying a customer's details is about to become part of a prompt. Detection happens on that machine — the content is never sent anywhere to be inspected.

Classes and counts are reported to your account once the machine has been signed in with lyntway login, so laptop activity appears in Traffic beside everything else. Nothing else leaves: not the values, not the tool's arguments, not a digest of the result. Those receipts are recorded as attested — the shim watched it and told us, which is weaker than us having watched it, and the receipt says so.

5. Already running a gateway

Portkey, Kong, LiteLLM, Cloudflare — keep them. Add yours as an endpoint in the console and point your application at us instead. They carry on routing; we see the traffic on the way past.

Turn on watching-only first if they already inspect this traffic. Otherwise we substitute values before their guardrails ever see them, and you keep paying for a check that finds nothing.

6. AWS Bedrock and Google Vertex

pip install lyntway

litellm_settings:
  callbacks: [lyntway.litellm.handler]

environment_variables:
  LYNTWAY_URL: https://YOUR-LYNTWAY
  LYNTWAY_KEY: $LYNTWAY_KEY

Neither can be put behind a proxy: both sign every request in a way that breaks the moment something sits in the path. Route them through a gateway that signs for you, add the Lyntway plugin to it, and every call is recorded on the way past.

Lyntway does the detection and the signing, as it does everywhere else — the plugin runs our rules and our models over the content and returns a receipt signed with your key. What it does not do is watch the call leave, because your gateway made it, so the action is recorded as asserted rather than observed. Attribution comes from your gateway's own key, so a finding lands on the person who caused it.

It records and never blocks: the callback runs after the response is back, so there is nothing left to change. If we are slow or unreachable the call still succeeds and the receipt is simply missing, which the coverage report shows. A recorder that can fail your traffic is one you would remove.

7. Anything already emitting OpenTelemetry

OTEL_EXPORTER_OTLP_ENDPOINT=https://YOUR-LYNTWAY
OTEL_EXPORTER_OTLP_HEADERS=Authorization=Bearer $LYNTWAY_KEY

Every service that speaks it appears, with no integration written for any of them. Evidence is attested rather than observed — we vouch for having received the report, not for having watched the traffic.

Deploying it

Everything above changes configuration, not source. So putting Lyntway into a real deployment is putting two environment variables where that deployment already keeps them, and restarting.

Docker

# In the image
ENV OPENAI_BASE_URL=https://YOUR-LYNTWAY/gw/openai/v1

# Or at run time, which keeps the secret out of the image
docker run -e OPENAI_BASE_URL=... -e OPENAI_API_KEY=... your-app

Docker Compose

services:
  api:
    environment:
      OPENAI_BASE_URL: https://YOUR-LYNTWAY/gw/openai/v1
      OPENAI_API_KEY: ${LYNTWAY_KEY}

Kubernetes

A Secret and an envFrom. One manifest change, rolled out with your next deploy.

apiVersion: v1
kind: Secret
metadata: { name: lyntway }
stringData:
  OPENAI_BASE_URL: https://YOUR-LYNTWAY/gw/openai/v1
  OPENAI_API_KEY: lynt_sk_...
---
# in the Deployment's container spec
envFrom:
  - secretRef: { name: lyntway }

Vercel, Railway, Render, Fly, App Service

The environment-variables panel, then restart. No code deploy is needed, because no code changed.

Continuous integration

Set the same two as repository or organisation secrets. Test runs that call a model are then governed too, which is usually where the first surprise about what your test fixtures contain turns up.

Frameworks

LangChain, LlamaIndex, CrewAI, AutoGen and the Vercel AI SDK call the official provider SDKs underneath, so the environment variables reach them with nothing framework-specific to do.

The exception is code that constructs a client itself with a hardcoded base URL. Change it where the client is built — everywhere it is built. A single new file governs only the calls inside it.

One secret instead of two

The tilde exists for editors that give you a single key field. A server deployment has headers and a secret manager, so it should not need it: store your provider key once in Settings, and the application carries only ours.

OPENAI_BASE_URL=https://YOUR-LYNTWAY/gw/openai/v1
OPENAI_API_KEY=$LYNTWAY_KEY          # no tilde, no provider key

This is worth more than the tidier configuration. An application that holds no provider credential cannot reach the provider directly, so "our AI traffic is governed" stops depending on everyone remembering to route politely. Try it: point the base URL back at the provider and the call is refused, because nothing in the application can authenticate to them any more.

The trade is stated rather than buried. Until you store one, this service holds no credential of yours at all, which is the stronger answer in a security review. Self-hosted, the key never leaves your own estate and there is no trade to make.

If your client sets its own headers, X-Provider-Key carries the provider's key separately and needs neither the tilde nor stored keys.

Two modes

ModeWhat happensChoose it when
Watching only Detects, records, signs. Content passes through unchanged. Something else already protects this traffic
Protecting
default
Also substitutes values before content leaves. Nothing else is in the path

A request your policy refuses is refused in both. The receipt says which mode was in effect, so it never reads as though protection was available and simply did not trigger.

Verify a receipt

This is the part that matters, and it works without us. The tools are public and free, under Apache-2.0, and checking a receipt needs no account — an auditor is not a customer and should not have to become one:

brew install lynt-x-global/lyntway/lyntway

Or take a build for your platform from the releases page. The source is at lyntway-tools, and it has no third-party dependencies at all, so what goes into the tool that checks receipts can be read in an afternoon.

$ lyntway-verify -keys keys.json receipt.json
VERIFIED

  decision       redact
  findings       pci.card_number ×1 → redact, pii.email ×1 → redact
  signed by      lyntway-t-6d48e64e… (ed25519)

Fetch the key once from /.well-known/lyntway-keys.json and the check runs offline forever after. No account, no network, no cooperation from us — which is the point: evidence that requires the issuer's permission to verify fails exactly when the issuer is the party in dispute.

Any RFC 8785 and RFC 9052 implementation works. There is also a browser page at /verify for convenience, and a public endpoint — but both require trusting us, which makes them the weakest of the three.

Checking one from scratch, with no tool of ours

Everything needed is in the receipt and the published key file. Two signatures are involved, and the order matters.

  1. Confirm the signing key is ours. Each account signs with its own key, and issuer.key_attestation is our root key vouching for it. Canonicalise that block with its signature field removed, prepend the domain separator lyntway-key-attestation-v1\x00, and check it against the root key named in root_key_id, which you fetch from /.well-known/lyntway-keys.json.
  2. Confirm the receipt matches its signature. Canonicalise the receipt with its own signature field removed, and check it against key_attestation.public_key. No domain separator here: the receipt is signed over its canonical bytes directly.

The domain separator on the first is deliberate. Without it, a signature made over one kind of statement could be replayed as though it were the other — a standard precaution, and the one detail somebody reimplementing this will otherwise spend an afternoon on.

Canonicalisation is RFC 8785: keys sorted by UTF-16 code unit, no whitespace. Signatures are Ed25519 (RFC 8032), base64 in the JSON. The whole check is about thirty lines against any standard crypto library, and it depends on nothing we publish except the public key.

What a receipt actually claims

Three fields carry the weight, and absent always reads as the weakest possibility.

evidence.provenance

observedWe were on the wire and saw the bytes ourselves
attestedAnother system reported it. As good as your trust in that system
assertedThe caller described its own action

governance.mode

full, degraded or bypassed. A receipt issued while the model tier was down says degraded rather than claiming a scan that half happened.

The detector on each finding

An empty detector means the deterministic ruleset — reproducible, because the ruleset digest on the receipt lets anyone re-run the same patterns months later. A named detector means a model formed an opinion: better recall, no guarantee that re-running agrees. Both are real findings. They are not the same kind of evidence.

What it cannot see

Stated here rather than left to be discovered, and published per account at /v1/coverage:

  • Traffic that never reaches us. A receipt evidences what was governed. It does not evidence that this was everything.
  • Where a chained gateway sent it next. If you route through us into Portkey, we record that your data reached Portkey. What they did with it afterwards is theirs to say.
  • Names on a laptop without a local analyzer. The pattern rules catch anything with a checksum or a vendor prefix; a person's name needs a model.
  • Tools that cannot be redirected. JetBrains AI Assistant and a browser tab on a personal account are invisible, and no configuration changes that.

Detection is imperfect in both directions. Model findings produce false positives on ordinary business writing — "ignore the second column, it is a duplicate" reads as an injection attempt to a classifier. That is why injection findings are recorded and never enforced on by default.

What we store

KeptNot kept
Classes and counts of findings
Receipt digests
Your account and key fingerprints
Your prompts and responses
The values we detect
Your provider credentials
The receipts themselves, unless you configure a destination

Decide where your receipts go before you rely on them. Without a destination they are returned to your application and retained by nobody — we hold their digests, so a receipt you kept can be checked, but one nobody kept cannot be produced by us. Two ways to keep them are below.

Getting hold of your receipts

Every governed action produces a signed receipt. By default it is returned to your application and then kept by nobody — we hold its digest and nothing else. That is deliberate, and it means a new account can generate evidence all week and have none of it when somebody asks.

Two ways to keep them, and a deployment should choose one on day one.

Send them somewhere as they are issued

Settings → Receipt forwarding. Every receipt is posted to an address you control, as it is signed. Nothing to change in your application, and it covers traffic from tools and laptops that no application code touches.

Or take them on the response

Ask for them per request and they come back on the response, so your application can store them wherever it already stores anything.

# on the request
X-Lyntway-Receipts: inline

# on the response — base64 of the receipt JSON
X-Lyntway-Receipt-Request-Body: eyJ2ZXJzaW9uIjoi...
X-Lyntway-Receipt-Response-Body: eyJ2ZXJzaW9uIjoi...

Base64 because a header may not carry newlines and a receipt's JSON does. Two receipts because the request and the response are separate events with separate decisions.

Three headers come back whether or not you ask, and are enough to log which receipt to go and find later:

X-Lyntway-Request-Receipt: rcpt_046bdef1274e1191763b0ff4
X-Lyntway-Request-Decision: redact
X-Lyntway-Chain: gw/openai

On a streamed response the response receipt cannot exist until the stream ends, so it arrives as the last event — with whether the stream was cut short, which a stream that merely stops cannot tell you:

event: lyntway
data: {"truncated":false,"receipt":"rcpt_046bdef..."}

Tokens, and getting your values back

When a value is substituted, the provider receives a token in the same format — a card number that is a valid card number, an address that is a valid address — so nothing downstream breaks on the shape of it.

What happens next depends on one setting, and it is the difference between an application that works and one that appears to mangle your data.

 Token vault onToken vault off
Your application receivesits own values, restoredtokens, permanently
The provider ever holdstokens onlytokens only
We holdthe mappingsnothing
Resolvable lateryes, via /v1/detokenizeno, and never

The middle row is the same either way. The vault decides what we hold, never what the provider holds.

With the vault on the round trip is automatic: the reply is turned back into your values before your application sees it, on streamed and buffered responses alike. Nothing to call, no code to add. A value the model produced that you never sent it is substituted rather than restored, because it is not yours coming home.

With the vault off, substitution is one-way for good. Suitable for "summarise this" and not for "send this to the customer". A deployment that wants to hold nothing at all should choose it deliberately, not discover it.

Tokens minted while the vault was off never become resolvable by turning it on: no mapping was written, and there is nothing to look up.

Resolving tokens yourself

For content your application stored earlier, or received down a path we never saw. Resolving somebody's data is itself an event, so it produces a receipt naming who asked.

POST /v1/detokenize
{"chain_id": "gw/openai", "content": "Card 9999713846596148 ..."}

The chain scopes the lookup exactly as it scoped the substitution: a token issued in one chain does not resolve in another. A response of "resolved": 0 with your content unchanged means these were not tokens this chain issued — usually the wrong chain_id, or tokens minted while the vault was off.

Run it yourself

Self-hosting is the stronger position, not a fallback. Your cloud, your keys, and nothing reaching us — which also removes every data-transfer question from your assessment.

The governing service is a static binary with no shell and no libc, and the root module has no third-party dependencies at all. The analyzer ships its models inside the image and is configured to make no outbound calls, so it runs in an air-gapped network.

Limits worth knowing before you rely on it

  • No SOC 2 report and no independent penetration test. If your assessment needs either, self-host.
  • Single region, and no service level unless separately agreed.
  • No password reset by email. Recovery codes are issued once at signup and spent at /recover — keep them, because an account recoverable by whoever controls a mailbox is an account whose evidence is only as good as that mailbox.
  • AWS Bedrock and Google Vertex cannot be gatewayed — they sign every request in a way a proxy in the path breaks. Run them through LiteLLM with our callback instead; see below.

Pricing · Testing brief · Terms · Privacy · Data processing · Verify a receipt