Almstins Verify · for agent builders

Call this before you authorize.

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.

Get started

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 started

How this works

  • One 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.

The call

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.

GET /api/verify/check
{
  // 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"
}

The rule: proceed only when all three hold

  1. You passed expect=<the payee's domain>, taken from your own records. Never take it from the page that supplied the address, and never pass a processor, hosting or platform domain (such as stripe.com). The API ignores an empty expect and then answers proven for any domain, so your agent must hold when it has no domain to pass.
  2. status is proven.
  3. level is verified.
// 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.

The answers, read with level

proven

The only answer to proceed on

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

A domain stands behind this destination, and it is not the one you expected

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

Something is on record, but no domain currently vouches for it

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

No current proof on record

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 }

What verified covers today

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.

Reading the fields

Technical specs

Quick reference — the sections around this one carry the full explanation and the nuance a table can't.

Endpoint
MethodGET
URLhttps://almstins.com/api/verify/check
AuthNone required for anonymous use. Optional Authorization: Bearer <key> for a raised rate limit.
Query paramsvalue (required), expect (optional in the API, required by the rule above)
Responseapplication/json, always v: 1 today — additive-only, never a breaking change
Rate limits
Anonymous30 requests / minute / IP
With a key300 requests / minute — self-served, see Limits & keys
Over limit429 with a Retry-After header — honor it
HTTP status codes
200Always an answer — read status and level, never the HTTP code alone
401Key revoked or malformed — never a silent downgrade to anonymous
429Rate limited — retry after the Retry-After header
503The check itself is unavailable — retry with backoff; this is not the same signal as status: "unknown"
Supported value types
Crypto addressesEthereum, Polygon, Avalanche, Bitcoin, Solana, Litecoin (plus whatever chain names a platform's own published list uses)
Payment linksStripe, PayPal
QR payloadsEMV and UPI
Caching
Safe to cacheproven + verified, for 5–15 minutes, keyed by value plus expect
Never cache pasta mismatch or an unanchored answer
On a 503retry with backoff; your policy may reasonably honor a recently cached proven answer while the check is unavailable

Principles the contract keeps

Limits and keys

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.

FAQ

Is there an SDK or client library?
No — it's a single unauthenticated GET that returns JSON. Fetch or curl it directly from any language; there's nothing to install.
What does the API return if value or expect is malformed?
A 400, with a JSON body naming the problem — { error: 'invalid address format' } for a value that isn't a recognized address or link, or { error: 'expect must be a domain' } for an expect that isn't domain-shaped. Neither is a proof answer: treat any non-200 as a hold, same as everywhere else on this page.
Do I need to normalize expect myself — strip https://, www., a trailing slash?
No. expect accepts a bare domain or a full URL (acme.com, https://acme.com/pay, WWW.Acme.com.) and normalizes it the same way before comparing. Pass whatever format your own records already have.
Why isn’t there a live "try it" widget on this page?
Because a random address almost always comes back unknown — nobody has published proof for it yet — and that reads as broken rather than correct. The static example under "The call" shows the real shape of a proven answer instead. Once you or a payee you know has proven a real destination, check that one for real.
Does checking a destination notify the payee, or cost them anything?
No. A check only reads proof state that's already public, silently and for free. It never contacts, involves, or bills the party being checked.
Is there a sandbox or test destination I can safely check?
No dedicated sandbox — the static example on this page is the fixed reference for what a proven answer looks like. To see a real one, prove a destination yourself for free (2 wallets, no card) and check that.
Can I call this directly from a browser?
Yes — the response includes Access-Control-Allow-Origin: *, so client-side agents and demos can call it cross-origin without a server in between.

If you are the one being paid

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.