Skip to content
Integra Agent Guard

Decline codes

Every code the gate can return, what it means, and which side can act on it.

A decline carries three things: a haltClass, a code, and a detail sentence written to be forwarded to the counterparty.

import type { GateDecision } from "@integraledger/agent-guard";

declare const decision: GateDecision;

if (decision.kind === "decline") {
  console.error(`[${decision.haltClass}] ${decision.code}: ${decision.detail}`);
}

Verification failures

The guard cannot say the document is the one that was advertised. The seller can act on all of these.

CodeRaised whenWhat the seller should do
gate/terms-unfetchablethe advertised terms URL did not serve — a non-2xx status, a TLS failure, a redirect, a body over the cap, or a host that failed the public-unicast checkserve the document at the URL advertised, over HTTPS, without a redirect
gate/fingerprint-mismatchthe recomputed SHA-256 over the served bytes ≠ the advertised atrHashre-advertise the hash of the bytes actually being served, or serve the bytes the hash was taken over

gate/terms-unfetchable carries the fetcher's own message verbatim, so the detail names which failure it was — the status, the cap, or the resolved address that was refused. That precision is the point: a buyer that cannot say which failure occurred cannot report it.

A redirect is a fetch failure here, deliberately. The fetcher sets redirect: "error", so a seller whose terms URL 301s to the real document sees gate/terms-unfetchable rather than a silent follow to wherever the redirect pointed.

Policy rejections

The document was established. Your stated policy refused it — the seller may be operating entirely correctly.

CodeRaised when
gate/below-required-levelthe proposal's level is below your requiredLevel
gate/assurancethe seller's assurance is below your requiredAssurance
gate/unparseable-termsthe terms are not machine-readable legal-context JSON, and your onNotAttempted is decline
policy/jurisdictionthe declared dispute jurisdiction is not in your acceptableJurisdictions
policy/dispute-methodthe declared dispute method is not in your acceptableDisputeMethods
policy/unityou declared no cap for the offer's unit
policy/over-capthe offer amount exceeds your cap for that unit
policy/forbidden-clausethe record declares a clause category you forbid

The seller's own defects

Also policy-rejection, but distinct in what they say: the field is present and malformed. These are named as the seller's defect rather than read as absence, because a refusal that misdescribes the defect cannot be acted on.

CodeRaised when
policy/malformed-dispute-resolutiondisputeResolution is not an object, or one of jurisdiction / method is present and not a string
policy/malformed-clause-categoriesclauseCategories is present and not an array

A seller publishing "disputeResolution": "arbitration" gets "your disputeResolution is a string, not an object" — not "jurisdiction (none) not acceptable", which would tell it to fix a jurisdiction that was never the problem.

Parser refusals

These are thrown, not returned, because they happen before a proposal exists — there is nothing yet to decide about. Fail-fast at the trust boundary.

Thrown whenMessage names
the challenge does not match the protocol's shapethe Zod validation failure
both x402 carriers advertise, and they disagreethe field, and both advertised values
neither x402 carrier advertises the fieldwhich field, and both carriers that were checked
the advertised hash is not 0x + 64 hexthe value received
the terms URL is not HTTPSthe URL
the offer amount is not a base-unit integerthe value received
a document matches more than one protocol's discriminantevery protocol that matched
the protocol is identified but this build cannot read its offerwhich protocols are parseable

Ambiguity refusing rather than guessing is covered in protocols.

Codes that are not declines

Two more codes exist, and neither is ever returned as a decision. gate/proceed and gate/escalate are what the record carries for the other two outcomes, so a log reader can tell the three apart by one field. A switch over decision codes never sees them.

Edit on GitHub

Last updated on

On this page