One GET answers the question your agent cannot feel its way through: does the business I intend to pay vouch for this destination? Proceed only when status is proven, level is verified, and you passed expect. Hold everything else for a human.
Free and anonymous to try — 30 checks a minute, no login. Building a fleet? Mint your own key with one DNS record for 300 a minute.
Log in to get startedOne free GET request
Pass the address and the domain you expect — anonymous, no key required.
Proceed only when proven + verified
Anything else is a hold for a human — never a guess.
Nobody approves anybody
Payees prove their own destinations. We only check the proof.
Cache your results
Proof state changes slowly — a few minutes is safe.
GET https://almstins.com/api/verify/check?value=<address or payment URL>&expect=<domain>
value is a crypto address, a payment link (Stripe, PayPal), or an EMV/UPI payload. expect is the domain of the business your agent intends to pay, taken from your own vendor record or the order you placed, never from the invoice, message or page that supplied the address: whoever swaps the address on an invoice can swap the domain on it too. Pass the payee's own domain at its registrable level (acme.com rather than pay.acme.com). expect also matches any subdomain of it, so never pass a processor, hosting or platform domain (stripe.com, github.io, pages.dev and similar); for a payee hosted on one, pass their full host. expect is optional in the API, and an empty expect is ignored, but the rule below requires it: always pass it.
{
// proceed only if status is "proven" AND level is "verified" AND you passed expect
"v": 1,
"status": "proven", // or "mismatch" | "unanchored" | "unknown"
"level": "verified", // "verified" | "claimed" | null — follows status; see below
"domain": "x-capital.com", // the domain that vouches for this destination, or null
"label": null, // a business name derived from domain, or null: display only, never decide on it
"chain": "ethereum",
"since": "2026-07-14", // date of the proof behind level (verified address: when its listing was first confirmed)
"proofAgeDays": 59, // age of that proof, not freshness (freshness lives in level)
"checkedAt": "2026-09-11T14:30:00Z"
} // payeeDomain: the payee's registrable domain from YOUR records, e.g. 'acme.com'
const want = String(payeeDomain ?? '').trim().toLowerCase();
if (!want) return holdForReview({ reason: 'no expect' }); // the API ignores an empty expect
const url = 'https://almstins.com/api/verify/check'
+ '?value=' + encodeURIComponent(value)
+ '&expect=' + encodeURIComponent(want); // always pass expect
let res;
try { res = await fetch(url); } catch { return retryWithBackoff(); }
if (res.status >= 500 || res.status === 429) return retryWithBackoff(res.headers.get('Retry-After'));
let body;
try { body = await res.json(); } catch { return holdForReview({ reason: 'unreadable answer' }); }
const got = String(body.domain ?? '').toLowerCase();
const mayProceed = res.ok
&& body.status === 'proven' && body.level === 'verified'
&& (got === want || got.endsWith('.' + want)); // the answer is about your payee
if (!mayProceed) return holdForReview(body); // flag it and escalate to a human Anything else is a hold: flag the payment and escalate to a human, or confirm the destination with the payee through a channel you already trust. proven and mismatch already only ever come back with level verified — checking level again costs nothing and protects your agent against a future bug in this API, so the sample above still checks both. The last line of mayProceed also checks that the answer's domain is the one you passed (or a subdomain of it), so an expect lost along the way cannot pass.
proven level is verified, and (you passed no expect, or the domain that anchors this destination matches it). Proceed under your own policy, and only when you passed expect: without it you only learn that some proof exists, not that it's your payee.
{ "status": "proven", "level": "verified", "domain": "acme.com" } mismatch level is verified — a domain currently vouches for this destination — but it is not the one you passed. The loudest signal this API can emit: your records say one business, the destination belongs to another. Hold the payment, flag it, and escalate to a human as a discrepancy to confirm with the payee through a channel you already trust, not as confirmed fraud: it can also come back when the payee genuinely operates more than one proven domain and you had the wrong one, or when you passed a subdomain that is not actually a subdomain of the one they proved.
{ "status": "mismatch", "level": "verified", "domain": "not-the-domain-you-expected.com" } unanchored level is claimed: control was proven (a self-send for a crypto address) or a payment link was registered to an account, but no domain backs it right now — never listed, removed from a listing, not re-confirmed within the last 24 hours, or (a payment link) the account holds no verified business name. Not fraud: a payee who has not reached Level 2, or whose proof has gone stale. Hold and confirm the destination with the payee through a channel you already trust.
{ "status": "unanchored", "level": "claimed", "domain": null } unknown level is null: no proof of this destination is on record at all — it was never proven, or the listing that was its only proof lapsed or was withdrawn. Not an accusation, just an absence: treat it the way a browser treats a site with no certificate. Fail closed: hold and escalate.
{ "status": "unknown", "level": null, "domain": null } verified means something different for each kind of destination. The rule above holds for all of them; this is what you rely on when you proceed.
status proven, mismatch, unanchored or unknown. proven and mismatch always come with level verified; unanchored always comes with level claimed; unknown always with level null — so status alone tells you which, but the rule below still checks level too, as belt-and-suspenders. Treat a status you do not recognize as a hold.level verified when a proof is current: for a crypto address, a listing on a proven domain re-confirmed within the last 24 hours; for a platform's list, a list refreshed within the last 24 hours; for a payment link, an account that holds a domain-verified business name (no ownership proof specific to the link, and no freshness check unless the link is monitored). claimed when control was proven by a self-send, or a payment link was registered, but no domain currently vouches for the destination. null when status is unknown.domain The domain that actually vouches for this destination right now — what your expect was matched against — or null when status is unanchored or unknown. Never the payee's business name on its own if it isn't also what anchors this destination, and never a payment link's own host (stripe.com, buy.stripe.com — that names the processor, not the payee).label A business name, shown only alongside a proven or mismatch answer, and only when it derives from domain — never the payee's raw freeform text, which we never treat as an identity. null otherwise, including whenever status is unanchored or unknown. Display it if you like, but never decide on it.chain The rail the destination is on: usually ethereum, polygon, avalanche, bitcoin, solana or litecoin, though a platform's published list may use its own chain names. url for any payment link or payment QR (EMV and UPI payloads included), or null.since The date of the proof behind level (YYYY-MM-DD). For a verified crypto address, the date its listing on a proven domain was first confirmed, never an older self-send date; for a claimed one, the date control was proven. For a payment link, the date it was registered; for a platform's list, the date the platform proved its domain. Or null. It follows level: when an address drops from verified to claimed, since moves to the date control was proven, which can be earlier.proofAgeDays Whole days since since. This is not freshness: an old proof can still be verified, and a lapsed listing reads claimed or unknown. Freshness lives in level. Because since follows level, proofAgeDays can jump when level changes.checkedAt When this answer was computed (ISO 8601, UTC).Quick reference — the sections around this one carry the full explanation and the nuance a table can't.
| Method | GET |
|---|---|
| URL | https://almstins.com/api/verify/check |
| Auth | None required for anonymous use. Optional Authorization: Bearer <key> for a raised rate limit. |
| Query params | value (required), expect (optional in the API, required by the rule above) |
| Response | application/json, always v: 1 today — additive-only, never a breaking change |
| Anonymous | 30 requests / minute / IP |
|---|---|
| With a key | 300 requests / minute — self-served, see Limits & keys |
| Over limit | 429 with a Retry-After header — honor it |
200 | Always an answer — read status and level, never the HTTP code alone |
|---|---|
401 | Key revoked or malformed — never a silent downgrade to anonymous |
429 | Rate limited — retry after the Retry-After header |
503 | The check itself is unavailable — retry with backoff; this is not the same signal as status: "unknown" |
| Crypto addresses | Ethereum, Polygon, Avalanche, Bitcoin, Solana, Litecoin (plus whatever chain names a platform's own published list uses) |
|---|---|
| Payment links | Stripe, PayPal |
| QR payloads | EMV and UPI |
| Safe to cache | proven + verified, for 5–15 minutes, keyed by value plus expect |
|---|---|
| Never cache past | a mismatch or an unanchored answer |
On a 503 | retry with backoff; your policy may reasonably honor a recently cached proven answer while the check is unavailable |
Anonymous calls are free and stay free: 30 requests per minute per IP, enough for a human-paced agent. For fleets, a key raises that to 300 per minute, and you issue it yourself: claim your domain from a free account, publish one DNS TXT record carrying the challenge, and the key mints. Nobody reviews an application, because there is no application. Send it as Authorization: Bearer <key>. A revoked or malformed key gets 401, never a silent downgrade, so your fleet knows the moment a key stops working.
Rate-limited calls return 429 with Retry-After. The check makes no outbound requests and reads only proof state that is already public — which is why keys can be self-served: a leaked key is a quota nuisance, not a breach.
Cache your results. Proof state changes slowly, so a proven answer with level verified is safe to cache for a few minutes (we suggest 5 to 15, keyed by value plus expect) rather than re-checking the same destination on every invoice in a batch. On a 503, retry with backoff, and your policy may reasonably honor a recently cached proven answer while the check is unavailable. Never cache your way past a mismatch or an unanchored, and never treat a 503 as unknown: an outage is not an absence of proof, which is exactly why the two are different signals.
Agents that follow this page proceed only on a verified answer. A self-send (Level 1) proves you control an address, and it answers unanchored, which careful agents hold for a human. To be paid by agents without a hold, reach Level 2, domain-verified: publish the small file on your site that proves your domain, and list your receiving addresses in it. The file is what makes an address verified. A DNS record also proves your domain and attaches your business name, but it lists no addresses, so on its own it leaves your addresses claimed. An address you already proved by a self-send becomes verified once you list it in the file and verify the domain, and goes back to claimed if the file stops listing it. If your dashboard asks you to prove a wallet again first, do that, then verify the domain again. A listed address stays verified only while your listing has been re-confirmed within the last 24 hours, and its since date is when the listing was first confirmed, not your older self-send. Neither level touches a wallet connection or a key: prove your domain and list your receiving addresses. An unproven destination is not accused of anything. It is simply invisible to an agent that checks, the way an unencrypted store is invisible to a careful customer.
More than a couple of addresses to list? Publish an encrypted roster instead of maintaining the plain file by hand: your browser encrypts the list to our public key before it ever leaves your computer, so the published file is unreadable to anyone but us, with nothing for your own hosting to check or protect. Adding or removing an address from the roster takes effect the next time it's checked, not on a delay. See the roster tool in your dashboard.
Platforms publishing many addresses serve a live list from their own domain at /.well-known/payment-addresses and connect it from a proven entity. The path carries no vendor's name on purpose: the format is meant to be boring, and anyone is free to serve or consume it. During early access, publishing a platform list is by approval: email support@almstins.com to request access.