Skip to main content
Glama

ASPERN: counterparty checks for agents

Read the transaction you are about to sign

inspect_payment

Call this with the CALLDATA you are about to sign, before you sign it. Every other check here asks whether an address is worth dealing with; this asks whether the transaction is the one you think it is, and the two catch different losses. No amount of reputation makes the recipient in the bytes match the recipient on your screen, and a spotless counterparty record says nothing about an UNLIMITED APPROVAL granted to it, which is not a payment at all but a standing permission that outlives the transaction. It reads three calls — transferWithAuthorization, approve, transfer — and REFUSES to guess at any other: decoding unknown calldata without the ABI means guessing where each argument begins, and a confident wrong answer about where money goes is worse than none. Amounts are atomic units, never dollars, because the token owns its decimals. Free, needs no key, stores nothing.

Input Schema

NameRequiredDescriptionDefault
toNoThe contract being called, where you know it. On an ERC-20 payment this IS the token: the calldata carries a recipient and an amount and never the asset, so without `to` there is nothing to compare the endpoint’s quoted asset against.
dataNoThe calldata, 0x-prefixed. Without it nothing about the transaction is read, and the answer says so rather than passing.
expectNoWhat you believe you are doing. Anything you leave out is not compared, and is reported as not compared rather than as agreeing.
resourceNoThe http(s) URL of the endpoint you are paying, where there is one. Send it and the answer also compares the bytes with the amount and the payee that endpoint demanded in its own 402 — the check that catches a transaction which is exactly what you meant and is not what the seller asked for.
refreshQuoteNoKnock on `resource` NOW instead of comparing against a stored reading, and set what comes back against the reading it replaced. `againstEndpoint.refreshed.quoteStillMatches` is true, false, or null where the two cannot be compared, with `verdict` as match/changed/unknown and `differs` naming which of amount, asset, network or payee moved. A CHANGED PAYEE is the substitution this whole check exists to catch. Only for an endpoint already in the x402 catalogue we read — we do not fetch a URL because a request named it — and at most one live handshake a minute per resource, shared by every caller. A match is what the endpoint said a moment ago, not a promise about the next moment.
maxQuoteAgeSecondsNoHow old a quote you are willing to act on, in seconds. YOURS, not ours: four hours is alarming for a market quote and irrelevant for a tariff unchanged since March, so a threshold published here would be a number we invented wearing the clothes of a measurement. The answer comes back as `againstEndpoint.standing.freshness` — `within-your-limit` or `older-than-your-limit` — with `freshEnough` as the boolean and your limit echoed back. Without it the answer says `no-limit-given` rather than grading the age.

Output Schema

NameRequiredDescriptionDefault
callNoWhat the bytes actually are, or null where it is not a call this decodes.
metaNoHow current this answer is and what it was read from: `freshness` and the ages behind it. Present on every answer from this server, whatever the tool.
limitsNoWhat decoding a payment establishes. It reads what the transaction SAYS it will do; it does not simulate it and cannot tell you the recipient is honest.
spenderNoWho would be allowed to move your tokens later. Only an approve() has one, and an approve() has nothing else.
verdictYes`read` means every comparison you asked for agreed. `mismatch` means at least one did not. `undecodable` means the call is not one this reads and NOTHING was checked — it is not a pass.
evidenceNoThe common payment record, the same object on every payment answer: `listedPayee` from the seller’s catalogue entry, `onChainRecipient` from the bytes, `recipientIsListedPayee` (false is a finding, null means one side is missing), `facilitator` (the technical route, never the seller), `settlement` (`unknown` here, because nothing has been sent), `delivery` (`unevidenceable` on x402, whose rail records the payment and never the delivery) and `says`.
findingsYesOne per check, each carrying whether it ran: observation, mismatch, or unknown.
selectorNoThe four-byte function selector the calldata begins with, where we could read one. Null means the payload is not a call we can decode, which is not evidence that it is bad.
warningsNoAnything the server wants a caller to know about THIS answer before acting on it. Empty is the normal case and is not a guarantee.
recipientNoWho RECEIVES the money. Null on an approve(), which pays nobody: branch on this rather than on the prose.
amountAtomicNoAtomic units, as a decimal string. The token owns its decimals and this does not apply them.
actualTransferNoWhether tokens move in THIS transaction. False for an approval, where the amount is a ceiling on what somebody else may take later rather than a sum leaving now.
againstEndpointNoSENT `resource`? THEN THIS COMPARES THE BYTES WITH WHAT THE ENDPOINT ITSELF IS DEMANDING, which is the one thing `expect` cannot catch: bytes that are exactly what you meant and are not what the seller is asking for. `amount` is `exact-match`, `below-quote`, `above-quote` or `unknown`, in BASE UNITS with no scaling and only where the token the call is made against is the token the quote prices in. **`below-quote` is the expensive one**: the call is refused and the tokens have left. `payee` is `matches`, `differs` or `unknown` against the address the 402 named, and it reads the approved SPENDER on an approval, which pays nobody. `asked: false` means we have never probed that endpoint, which is our gap and not evidence about it. `verdict` above is UNCHANGED by any of this — that is the manifest against YOUR expectation, and the two questions are kept apart on purpose.
unlimitedAllowanceNoTrue where this grants permission to move every token of this kind you will ever hold.

Schema Changelog

Changes observed during successful MCP inspections.

  1. Changed1 schema field changed
    • addedOutput schema / properties / evidence
      Added value: +{
      +  "description": "The common payment record, the same object on every payment answer: `listedPayee` from the seller’s catalogue entry, `onChainRecipient` from the bytes, `recipientIsListedPayee` (false is a finding, null means one side is missing), `facilitator` (the technical route, never the seller), `settlement` (`unknown` here, because nothing has been sent), `delivery` (`unevidenceable` on x402, whose rail records the payment and never the delivery) and `says`.",
      +  "type": "object"
      +}
  2. Changed2 schema fields changed
    • addedOutput schema / properties / meta
      Added value: +{
      +  "description": "How current this answer is and what it was read from: `freshness` and the ages behind it. Present on every answer from this server, whatever the tool.",
      +  "type": "object"
      +}
    • addedOutput schema / properties / warnings
      Added value: +{
      +  "description": "Anything the server wants a caller to know about THIS answer before acting on it. Empty is the normal case and is not a guarantee.",
      +  "items": {
      +    "type": "string"
      +  },
      +  "type": "array"
      +}
  3. Changed2 schema fields changed
    • addedInput schema / properties / maxQuoteAgeSeconds
      Added value: +{
      +  "description": "How old a quote you are willing to act on, in seconds. YOURS, not ours: four hours is alarming for a market quote and irrelevant for a tariff unchanged since March, so a threshold published here would be a number we invented wearing the clothes of a measurement. The answer comes back as `againstEndpoint.standing.freshness` — `within-your-limit` or `older-than-your-limit` — with `freshEnough` as the boolean and your limit echoed back. Without it the answer says `no-limit-given` rather than grading the age.",
      +  "type": "number"
      +}
    • addedInput schema / properties / refreshQuote
      Added value: +{
      +  "description": "Knock on `resource` NOW instead of comparing against a stored reading, and set what comes back against the reading it replaced. `againstEndpoint.refreshed.quoteStillMatches` is true, false, or null where the two cannot be compared, with `verdict` as match/changed/unknown and `differs` naming which of amount, asset, network or payee moved. A CHANGED PAYEE is the substitution this whole check exists to catch. Only for an endpoint already in the x402 catalogue we read — we do not fetch a URL because a request named it — and at most one live handshake a minute per resource, shared by every caller. A match is what the endpoint said a moment ago, not a promise about the next moment.",
      +  "type": "boolean"
      +}
  4. Changed3 schema fields changed
    • addedInput schema / properties / resource
      Added value: +{
      +  "description": "The http(s) URL of the endpoint you are paying, where there is one. Send it and the answer also compares the bytes with the amount and the payee that endpoint demanded in its own 402 — the check that catches a transaction which is exactly what you meant and is not what the seller asked for.",
      +  "type": "string"
      +}
    • changedInput schema / properties / to / description
      Previous value: -"The contract being called, where you know it."New value: +"The contract being called, where you know it. On an ERC-20 payment this IS the token: the calldata carries a recipient and an amount and never the asset, so without `to` there is nothing to compare the endpoint’s quoted asset against."
    • addedOutput schema / properties / againstEndpoint
      Added value: +{
      +  "description": "SENT `resource`? THEN THIS COMPARES THE BYTES WITH WHAT THE ENDPOINT ITSELF IS DEMANDING, which is the one thing `expect` cannot catch: bytes that are exactly what you meant and are not what the seller is asking for. `amount` is `exact-match`, `below-quote`, `above-quote` or `unknown`, in BASE UNITS with no scaling and only where the token the call is made against is the token the quote prices in. **`below-quote` is the expensive one**: the call is refused and the tokens have left. `payee` is `matches`, `differs` or `unknown` against the address the 402 named, and it reads the approved SPENDER on an approval, which pays nobody. `asked: false` means we have never probed that endpoint, which is our gap and not evidence about it. `verdict` above is UNCHANGED by any of this — that is the manifest against YOUR expectation, and the two questions are kept apart on purpose.",
      +  "type": [
      +    "object",
      +    "null"
      +  ]
      +}
  5. Changed3 schema fields changed
    • addedOutput schema / properties / actualTransfer
      Added value: +{
      +  "description": "Whether tokens move in THIS transaction. False for an approval, where the amount is a ceiling on what somebody else may take later rather than a sum leaving now.",
      +  "type": "boolean"
      +}
    • changedOutput schema / properties / recipient / description
      Previous value: -"Who the bytes pay, read from the payload rather than from a label."New value: +"Who RECEIVES the money. Null on an approve(), which pays nobody: branch on this rather than on the prose."
    • addedOutput schema / properties / spender
      Added value: +{
      +  "description": "Who would be allowed to move your tokens later. Only an approve() has one, and an approve() has nothing else.",
      +  "type": [
      +    "string",
      +    "null"
      +  ]
      +}
  6. Changed2 schema fields changed
    • addedOutput schema / properties / limits / description
      Added value: +"What decoding a payment establishes. It reads what the transaction SAYS it will do; it does not simulate it and cannot tell you the recipient is honest."
    • addedOutput schema / properties / selector / description
      Added value: +"The four-byte function selector the calldata begins with, where we could read one. Null means the payload is not a call we can decode, which is not evidence that it is bad."
  7. Added

TDQS

A4.5/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations are thin (readOnlyHint false, openWorldHint true, not idempotent, not destructive), and the description carries the real weight: it discloses the three-call selector whitelist, that it refuses rather than guesses on unknown ABI, that amounts are atomic units, that it stores nothing and needs no key, and a rate limit of one live handshake per minute per resource. That is exactly the context annotations cannot express.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the imperative and the alternative-check contrast, and nearly every clause carries information (selectors supported, refusal policy, atomic units, no storage). It is denser and more rhetorically styled than needed, which slows scanning, but there is no filler to cut.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a six-parameter, nested-object tool with an output schema, the description covers the behavioral envelope an agent needs — refusal on unknown calldata, what expect omission means, quote-freshness semantics, live-refresh limits. What it does not say is what the answer looks like when called with nothing at all, a minor gap given the output schema handles return shape.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100% and the nested expect/resource/freshness semantics are already spelled out in the schema descriptions, so the description is mostly restating. It does reinforce that calldata is mandatory in practice and that atomic units are not dollars, but adds little beyond the schema. Baseline 3 is right.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and object — read the calldata you are about to sign and check it against what you believe you are doing — and explicitly contrasts that with what the sibling checks do ('every other check here asks whether an address is worth dealing with'). An agent can place it against preflight_payment and counterparty_check without opening any schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives the trigger ('before you sign it'), the exclusion (no amount of reputation substitutes for matching bytes), and the tool's own refusal boundary — it reads only transferWithAuthorization, approve and transfer and refuses any other calldata. It even sketches when the optional refreshQuote path applies and its rate limit.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Try in Browser

Glama MCP Gateway

Add one secure layer between your agents and this server.

Resources