Verify ownership of your API
A Lightning Enable proxy sits in front of an API (its target) and charges a small Lightning payment for each call. It uses L402, an HTTP payment protocol: a caller without a credential gets a payment challenge (an HTTP 402 response carrying a Lightning invoice), pays the invoice, and retries with proof of payment. Anyone with a Lightning Enable account can point a proxy at any public URL, so before a proxy appears in public listings, Lightning Enable asks the proxy's owner to prove that they control the target. This page shows you how to do that.
You publish a token that only you received, in a place only the target's owner can write to, and then ask Lightning Enable to look for it. That place is one of three: a small JSON file on the target's host, a DNS TXT record, or a response header from the API itself.
What verification gives you
Verification is a tier, not a gate. An unverified proxy works exactly as it did before: callers can reach it by its direct URL, pay, and get answers. Verification adds three things:
- Public listing. Only a verified proxy can be listed in the public L402 registry (the directory of paid APIs that agents search to find services), in agent discovery (
/api/agents/capabilities, the same list served to agents), and on Nostr (a public relay network where Lightning Enable also announces listed APIs). If you try to list an unverified proxy,PUT /api/proxy/{proxyId}/manifest/settingswith"manifestPubliclyListed": trueanswers422with"error": "ownership_verification_required"and averify_endpointto call first. - Higher challenge caps for claim accounts. If you created your account with the one-payment claim (
POST /api/producer/claim, which creates a Lightning Enable account and its first proxy with a single Lightning payment), any unverified proxy of that account runs under a per-minute and per-month limit on payment challenges, including proxies you add later through the API or the dashboard. Past the limit, callers get429with"error": "unverified_proxy_rate_limited"and aRetry-Afterheader. Verifying a proxy removes that limit for it; your plan's caps then apply alone. Proxies of accounts created any other way never have this limit. - The verified mark. Your proxy's manifest (
serviceblock) and its registry entry carryverified,verification_method,verified_at, andlast_checked_at. Buyers and their agents read these fields to tell a proxy run by the API's owner from one run by someone else.verified_atmeans verified since: it's when the current method first found your token, and later successful checks don't move it.last_checked_atis when the latest check ran, including the daily re-check. Both are UTC to the second, ending inZ. A grandfathered listing showsverified: falsewithverification_method: "grandfathered": it's listed because it predates verification, and nothing was proven.
Verification belongs to the target it was proven on. If you change a proxy's target URL, the proxy becomes unverified and is unlisted; see Change the target URL.
Before you start
You need:
- A Lightning Enable merchant API key, sent as the
X-API-Keyheader on every call below. You received it when you created your account: in theapiKeyfield of the one-payment claim response, or in Dashboard → Settings → API Credentials, where you can also regenerate it. - The proxy's ID, for example
forecast-api-a1b2. Find it inproxy.proxyIdin the claim response, in the response ofPOST /api/proxythat created the proxy, on the dashboard's proxy list, or by listing your proxies withGET /api/proxy. The examples on this page use a proxy whose target base URL ishttps://api.example.com/v1. - The ability to change one of these: the files served at the target host's web root, the DNS records for the target's hostname, or the API's response headers.
All calls go to https://api.lightningenable.com.
Step 1: Get a verification token
If you created the proxy with the one-payment claim, the claim response already contains a token in verification.token. Use it and skip to step 2.
Otherwise, issue a token:
curl -X POST https://api.lightningenable.com/api/proxy/forecast-api-a1b2/verification/token \
-H "X-API-Key: $LIGHTNING_ENABLE_API_KEY"
The response's token field holds the token: a 43-character string of letters, digits, -, and _. This is the only time you see it. Lightning Enable stores only a hash of it, so it can't show it to you again. If you lose it, issue a new one.
Keep these rules in mind:
- The token proves ownership for listing purposes only. It can't call the API, move money, or change your account. It's still a credential: don't share it beyond the place you publish it.
- Issuing a new token replaces the old one. The old token stops working, and the proxy becomes unverified and unlisted until you publish the new token and verify again. The one exception is a grandfathered proxy, which stays verified and listed.
- Each proxy has its own token. Two proxies on the same host need two tokens.
Step 2: Publish the token
Choose one method, publish the token, and go to step 3. You don't need more than one.
Verification methods
Lightning Enable tries the methods in this order and stops at the first one that finds your token. The method name is the value you see in the method field after a successful check.
| Method | Where the token goes | Choose it when |
|---|---|---|
well-known | A JSON file at https://{target host}/.well-known/lightning-enable.json | You control the web server for the target's hostname and can add a file at its root. |
dns-txt | A DNS TXT record named _lightning-enable.{target host} | Someone else hosts the API, but you manage DNS for its hostname (a custom domain on a hosting platform, for example). |
header | An X-Lightning-Enable-Verify response header at the target's base URL | You own the API's code but not the web root or DNS, for example an API mounted under a path on a shared domain. |
Each method proves something different. The file proves that you control the web root of the target host, the TXT record proves that you control the DNS zone for the target's hostname, and the header proves only that you control the API's responses at the target base URL. Buyers see which method you used in verification_method.
In every example, replace <token> with your token. The target host is the hostname of the proxy's target base URL, including any subdomain: for https://api.example.com/v1, it's api.example.com, not example.com.
Well-known file
Serve this file over HTTPS on port 443, with a valid, publicly trusted certificate for the target host, at the root of that host. Lightning Enable fetches it over HTTPS even when your target base URL uses http. The path is the same whatever path your target base URL has.
URL:
https://api.example.com/.well-known/lightning-enable.json
Body:
{"verify":"<token>"}
To verify several proxies that point at the same host, list every token in an array:
{"verify":["<token for proxy 1>","<token for proxy 2>"]}
The file must answer 200 (any 2xx status works), be valid JSON, and stay under 4 KB.
DNS TXT record
Add one TXT record:
| Field | Value |
|---|---|
| Name | _lightning-enable.api.example.com |
| Type | TXT |
| Value | <token> |
In zone-file form:
_lightning-enable.api.example.com. 300 IN TXT "<token>"
The value is the token and nothing else: no prefix such as lightning-enable-verify=. Several TXT records can share the name, which lets you verify several proxies on one host.
Many DNS providers add your domain to the name you enter. If your zone is example.com, enter _lightning-enable.api in the name field; entering the full name can create _lightning-enable.api.example.com.example.com.
Response header
Return this header from the target's base URL, with a trailing slash (https://api.example.com/v1/ for the example proxy). Lightning Enable reads the base URL's scheme, host, and path only: if the target URL has a query string or a fragment, it isn't sent.
X-Lightning-Enable-Verify: <token>
Lightning Enable sends HEAD first and then GET to that URL. Any status counts except a redirect (3xx), so an API root that answers 401 or 404 works, as long as the header is on the response. Lightning Enable reads only the headers, never the body. To carry several tokens, separate them with commas.
Step 3: Run the check
curl -X POST https://api.lightningenable.com/api/proxy/forecast-api-a1b2/verify \
-H "X-API-Key: $LIGHTNING_ENABLE_API_KEY"
A successful check looks like this (the instructions block is shortened here; see the API reference for all of it):
{
"proxyId": "forecast-api-a1b2",
"verified": true,
"method": "dns-txt",
"verifiedAt": "2026-11-08T18:02:11Z",
"lastCheckedAt": "2026-11-08T18:02:11Z",
"failures": 0,
"tokenIssued": true,
"token": null,
"listed": false,
"nextCheckAt": "2026-11-09T04:00:00Z",
"checkedAt": "2026-11-08T18:02:11Z",
"reason": null,
"pending": false,
"retryAfterSeconds": null,
"instructions": { "...": "..." }
}
verified: true means the check found your token, and method says how. listed stays false until you turn on listing yourself:
curl -X PUT https://api.lightningenable.com/api/proxy/forecast-api-a1b2/manifest/settings \
-H "X-API-Key: $LIGHTNING_ENABLE_API_KEY" -H "Content-Type: application/json" \
-d '{"manifestEnabled": true, "manifestPubliclyListed": true}'
Read a failed check
When no method finds the token, verified is false and reason explains what each method saw, in order, separated by semicolons:
{
"verified": false,
"failures": 1,
"reason": "well-known: /.well-known/lightning-enable.json answered HTTP 404; dns-txt: no TXT record at _lightning-enable.api.example.com; header: HEAD answered HTTP 401 without X-Lightning-Enable-Verify; GET answered HTTP 401 without X-Lightning-Enable-Verify"
}
Look at the part for the method you chose; the other two failing is expected. The reason never contains your token. Fix what it names and call verify again.
If the reason starts with The target host was refused before any request, no method ran; see Private or internal host.
A pending check
Each check has an overall time limit. If your target is slow, the check can run past it. Then the response has pending: true and retryAfterSeconds (also sent as a Retry-After header), and reason says what the check learned before it stopped. A pending check records nothing: it doesn't count as a failure. Wait the number of seconds given and call verify again.
Check status without running a check
curl https://api.lightningenable.com/api/proxy/forecast-api-a1b2/verification \
-H "X-API-Key: $LIGHTNING_ENABLE_API_KEY"
This returns the same state and the exact instructions for this proxy (file URL, TXT record name, header name and URL), with <token> in place of the token. It never returns the token itself.
Both POST calls allow 10 requests per minute per API key.
Stay verified
Lightning Enable re-checks every active, verified proxy once a day, at 04:00 UTC; nextCheckAt shows the next run. nextCheckAt is null when the proxy isn't re-checked: while it's unverified or inactive, and for grandfathered and platform proxies. Leave the token in place for as long as you want the proxy to stay verified.
- A passing re-check resets
failuresto0. - A failing re-check adds one to
failures. The proxy stays verified. - When a daily re-check finds three consecutive failed checks, the proxy becomes unverified:
verifiedturnsfalse, the proxy is unlisted from the registry, agent discovery, and Nostr, and a claimed proxy is limited again. The proxy itself keeps working by its direct URL; Lightning Enable never deletes or deactivates it.
To recover, publish the token again (or issue a new one), call verify, and turn listing back on with PUT /api/proxy/{proxyId}/manifest/settings. Listing doesn't come back by itself.
The daily re-check has a longer time limit than the verify endpoint, long enough for a slow server to answer. A re-check that still gets no answer within it counts as a failure, so a server that stops responding loses verification within three days, like one that answers with the wrong token. A failed verify call counts toward the three as well, so it can happen sooner.
Proxies listed before verification
A proxy that was already publicly listed when ownership verification was introduced stays listed. Its method (and verification_method in the manifest and registry) is grandfathered, and verified is false: it's listed because it predates verification, not because ownership was proven. The daily re-check skips it, so it never loses its listing that way, and issuing a token doesn't unlist it. Changing its target URL does: see Change the target URL.
grandfathered isn't a method you can choose. To replace it with a real one, issue a token, publish it by one of the three methods, and call verify. Until a check succeeds, the proxy keeps its grandfathered standing.
Shopify store listings
Shopify store entries that Lightning Enable operates are verified by the platform, because their target is Lightning Enable's own host, not a server you run. Their method (and verification_method) is platform, and verified is true. Like grandfathered, platform isn't a method you can choose, and the daily re-check skips it. Shopify entries that were already listed when ownership verification was introduced were stamped grandfathered at first, and they're upgraded to platform the next time Lightning Enable syncs the store's registry entry. That sync runs when the service starts and whenever the store's listing changes. Delisting and relisting an entry doesn't change this.
Change the target URL
Verification proves control of one target. When you change a proxy's target URL with PUT /api/proxy/{proxyId} and a new targetBaseUrl (or with any later dashboard control that edits the target), Lightning Enable clears its verification and unlists it, whatever the method, grandfathered included. Only platform entries keep their standing. The proxy keeps working by its direct URL, and your token stays valid: publish it for the new target, call verify, and turn listing back on with PUT /api/proxy/{proxyId}/manifest/settings. A change that only adds or removes a trailing slash doesn't count.
Troubleshooting
Private or internal host
Reason: The target host was refused before any request: ...
Lightning Enable checks only public hosts. A target that resolves to a private, loopback, or link-local address, or a hostname that doesn't resolve, is refused before any request or DNS TXT lookup. The proxy can't forward to that host either, so fix the target: point the proxy at the API's public HTTPS address.
Redirects
Reason: answered with a redirect (HTTP 301), which is not followed
Lightning Enable never follows redirects, even one that would lead to the token. Common causes:
- The target host redirects
/.well-known/...to another host (apex towww, or to a docs site). Serve the file on the target host itself. - The target base URL lacks a trailing slash and the server redirects
/v1to/v1/, or the reverse. Make the base URL answer directly with the header. - An authentication layer redirects to a sign-in page. Exempt
/.well-known/lightning-enable.json(or theHEADrequest to the base URL) from it.
A CDN or cache serves an old answer
If a CDN sits in front of the target, it can keep serving a cached 404 from before you added the file, or an old file after you issue a new token. The reason then says the file answered 404 or "verify" does not contain this proxy's current token. Purge /.well-known/lightning-enable.json from the cache, or serve it with Cache-Control: no-cache. Headers can be cached the same way at the base URL.
DNS changes haven't propagated
Reason: no TXT record at _lightning-enable.api.example.com
A new record can take a few minutes to reach every resolver, and a changed record can take as long as the old record's TTL. Check what public DNS returns:
dig +short TXT _lightning-enable.api.example.com
nslookup -type=TXT _lightning-enable.api.example.com
If the command returns nothing, wait and try again. A resolver that looked up the name before the record existed can also cache that "no record" answer for a while (the negative-caching time set in your zone's SOA record), so a lookup soon after an early failed check can stay empty for longer than the new record's TTL. If it returns a different value, look for a record that your provider created under a longer name (see DNS TXT record) or an old token.
The check times out
Reason: no answer within ...s, or a response with pending: true
Each request has its own short timeout, and the whole check has an overall limit. A target that is slow to answer, or a DNS server that is slow to respond, can run past either one. Make sure the well-known file or the base URL answers quickly without waking a cold-start backend, then call verify again after retryAfterSeconds. If one method is slow, use another: a DNS TXT record doesn't depend on your server's response time.
Connection and certificate errors
Reason: could not be reached (...) or failed (...)
Lightning Enable couldn't complete the request. The word in parentheses names the cause: for example NameResolutionError (the hostname doesn't resolve), ConnectionError (nothing answered on the port, or the connection was refused), or SecureConnectionError (the TLS handshake failed, often an expired, self-signed, or wrong-host certificate). A failed (...) reason names an unexpected error type. Check that the host answers HTTPS on port 443 with a valid certificate, for example with curl -I https://api.example.com/.well-known/lightning-enable.json, and that a firewall doesn't block requests from outside your network.
The target URL can't be verified
Reason: The target URL cannot be verified: ...
The proxy's target base URL itself is not one Lightning Enable checks, so no method ran. The rest of the reason says why: the URL uses an IP address instead of a hostname, points at localhost or an internal domain, uses a port other than 80 or 443, or uses a scheme other than http or https. Update the proxy's target to the API's public hostname, then issue a token and verify again.
Other reasons
| Reason contains | What to do |
|---|---|
is not valid JSON | Serve exactly {"verify":"<token>"}, with straight quotes and no comments. |
has no "verify" field | The key must be verify, in lowercase. |
is larger than 4096 bytes | Serve only the verify field, not a larger document. |
/.well-known/lightning-enable.json answered HTTP 404 | The file isn't at the host root. Check the path and that it's served on the target host itself. |
"verify" must be a string or an array of strings | Put the token in quotes, or in an array of quoted tokens. |
"verify" does not contain this proxy's current token | The file's token is old or mistyped. Publish the token you most recently issued, or issue a new one. |
no TXT record at _lightning-enable.api.example.com contains this proxy's current token | A TXT record exists at the name, but none of its values is exactly your current token. Remove quotes, prefixes, and old tokens, or issue a new token. |
answered HTTP 401 without X-Lightning-Enable-Verify | The base URL answered, but without the header. Add it to every response at the base URL, including error responses and HEAD. |
X-Lightning-Enable-Verify does not carry this proxy's current token | The header is there, but its value isn't your current token. Publish the token you most recently issued. |
the host check did not finish within ...s or the host check failed (...) | Lightning Enable couldn't look up the target host in time. Try again; if it persists, check the hostname's DNS. |
No verification token (409, verification_token_required) | No token exists for this proxy. Issue one. |
Next steps
- Sell With Your Agent: create a paid proxy end to end.
- Producer API Reference: full request and response fields for the verification endpoints.
- Proxy Configuration: manifest settings, including public listing.