Most e-commerce platforms have some version of product availability.
A product is active. It is in stock. It has a price. It can appear on the storefront.
For a human-facing e-commerce site, that model can be good enough for a long time. The storefront can render a product page, show an “Add to cart” button, and let checkout perform final validation later.
For agent-ready commerce, that model becomes too vague.
The problem is not that available is wrong. The problem is that it hides too many different decisions behind one word.
Available for what?
Discovery?
Comparison?
Policy quotation?
Cart insertion?
Checkout preparation?
Delegated payment?
Those actions do not carry the same risk, and they should not use the same rule.
Series context
This is the third article in the Agent-Ready Commerce series.
Part 1 introduced the broader model:
Facts → Eligibility → Authority → State transition → Evidence → Audit
Part 2 focused on the first part of that chain: commercial truth. A raw product record is not enough for agent-facing systems. The platform needs source-backed, freshness-aware product facts before software can safely act on product information.
This article focuses on the second part of the chain: eligibility.
The main idea is to answer four separate questions instead of hiding them behind available:
Commercial truth says what is known.
Eligibility says which actions are valid.
Authority says who is allowed to request them.
Execution performs the state change.
Keeping those layers separate prevents a valid fact from being mistaken for permission, and permission from being mistaken for successful execution.
The problem with generic availability
A typical product model might contain fields like this:
type Product = {
id: string;
title: string;
active: boolean;
inventoryStatus: "in_stock" | "low_stock" | "out_of_stock";
price: {
amount: number;
currency: string;
};
};
A simple availability check might look like this:
function isAvailable(product: Product) {
return product.active && product.inventoryStatus !== "out_of_stock";
}
This is not a bad function. It answers a real question.
The issue is that the question is too broad.
If isAvailable(product) returns true, what exactly did the platform allow?
Can the product appear in search?
Can an agent recommend it?
Can an agent compare it with alternatives?
Can an agent quote its return policy?
Can an agent place it in a cart?
Can an agent prepare checkout?
Can an agent trigger a delegated payment flow?
A human-facing storefront may not need to separate all of those actions immediately. A page can render the product and rely on later validation to catch problems.
An agent-facing system needs a more precise contract. If an external agent sees available: true, it may reasonably assume that the product can be used in a commercial action. That is too much meaning for one boolean.
The platform needs action-specific eligibility.
A running example
Consider a product that appears simple from the catalog perspective:
Product: Travel Backpack
SKU: BAG-TRAVEL-42
Price: €129
Catalog status: active
Inventory status: in_stock
Category: Travel Bags
A storefront can render this product. It has a title, price, image, category, and inventory flag.
The commercial truth layer, however, may know more:
Price: fresh
Inventory: stale
Return policy: missing for Travel Bags
Warranty policy: known
Shipping policy: known for EU, unknown for US
Generated description: pending review
Feed publication: last published yesterday
A generic availability model might still say:
available = true
But that answer is not precise enough.
A better action-level view would be:
discover allowed
compare allowed
quote_policy blocked
add_to_cart requires_revalidation
prepare_checkout blocked
delegate_payment blocked
This is the important shift.
The product is not simply available or unavailable. It has different readiness levels for different actions.
A human user may still browse the product page. An agent may still discover and compare the product. But the platform should not allow policy quotation or checkout preparation until the missing and stale facts are resolved.
Actions need to be part of the domain model
The first step is to name the actions that matter at the agent boundary.
A platform does not need to expose every internal operation, but it should define the commercial actions agents may request.
For example:
type AgentCommerceDecisionAction =
| "discover"
| "compare"
| "quote_policy"
| "add_to_cart"
| "prepare_checkout"
| "delegate_payment"
| "complete_checkout"
| "show_generated_claim"
| "explain";
These actions are not interchangeable.
discover means the product may appear in an agent-facing catalog, search result, or product feed.
compare means the product may be evaluated against alternatives using selected commercial facts.
quote_policy means the agent may describe policy terms such as returns, shipping, cancellation, or warranty.
add_to_cart means the agent may place the product into a draft cart or buyer-intent context.
prepare_checkout means the agent may start or prepare a checkout session.
delegate_payment means the agent may request payment within a bounded authority scope tied to the current checkout.
complete_checkout means the platform may finalize a valid checkout through payment and order submission when all state, authority, and idempotency requirements pass.
show_generated_claim means the platform may project approved generated language for the requested surface and use.
explain means the platform may return an explanation from the existing decision basis without recomputing the decision locally.
Once the actions are named, the platform can stop treating product availability as one global state.
Instead, it can ask:
Given the current product truth and request context,
is this specific action allowed?
That is a better question.
Different actions require different facts
Action eligibility should depend on commercial truth, but not every action requires the same facts.
A simplified requirement matrix might look like this:
Action Required truth Example result
--------------------------------------------------------------------------------
discover identity, visibility, media allowed
compare identity, price, comparable attributes allowed
quote_policy applicable policy coverage blocked
add_to_cart price, inventory signal requires_revalidation
prepare_checkout fresh price, fresh inventory, policies blocked
delegate_payment valid checkout state, mandate, payment authority blocked
complete_checkout valid checkout, payment result, order transition blocked
show_generated_claim approved claim, scope, surface, use, evidence requires_review
explain existing decision basis and safe evidence allowed
This matrix is more useful than a single available flag because it explains what each action depends on.
The travel backpack example becomes clearer:
discover
Required: identity, visibility, media
Result: allowed
compare
Required: identity, price
Result: allowed
quote_policy
Required: return-policy coverage
Result: blocked because return policy is missing for Travel Bags
add_to_cart
Required: price, inventory signal
Result: requires_revalidation because inventory is stale
prepare_checkout
Required: fresh price, fresh inventory, applicable policies
Result: blocked because inventory is stale and return policy is missing
delegate_payment
Required: valid checkout state and payment authority
Result: blocked because checkout is not ready, so payment is not attempted
This does not mean the product is broken. It means different capabilities have different readiness levels.
That distinction is essential for agent-ready systems.
Eligibility decisions should be structured
A boolean return value is rarely enough because it shows the outcome but not the reason, evidence, freshness, or safe next step.
This is too weak:
const checkoutAllowed = false;
It tells the caller the result, but it does not explain the decision or preserve enough meaning for other surfaces.
A more useful eligibility result includes action, subject, context, result, blocker codes, evidence references, and next safe actions. In a decision-spine architecture, that eligibility result becomes one section of a broader envelope rather than the whole answer.
type AgentCommerceDecisionEnvelopeAuthenticator =
| {
kind: "digital_signature";
algorithm: "ed25519";
format: "detached";
keyId: string;
verificationKeyRef: string;
protectedHash: string;
value: string;
verifiable: true;
}
| {
kind: "message_authentication_code";
algorithm: "hmac-sha256";
format: "detached";
keyId: string;
verificationKeyRef: string;
protectedHash: string;
value: string;
verifiable: true;
}
| {
kind: "unsigned";
algorithm: "none";
format: "none";
protectedHash: string;
verifiable: false;
warning: "missing_platform_signing_key";
};
type AgentCommerceDecisionEnvelope = {
contractVersion: "agent-commerce-decision-envelope-v4";
envelopeSchemaVersion: "agent-commerce-decision-envelope-schema-v4";
decisionId: string;
decisionHash: string;
inputDependencyHash: string;
resultHash: string;
ruleSetVersion: string;
ruleSetRef: string;
ruleSetHash: string;
authenticator: AgentCommerceDecisionEnvelopeAuthenticator;
requestedAction: AgentCommerceDecisionAction;
basis: {
status:
| "allowed"
| "blocked"
| "requires_revalidation"
| "requires_review"
| "requires_confirmation";
allowed: boolean;
reasonCodes: readonly string[];
components: readonly {
code: string;
source: "eligibility" | "authority" | "checkout" | "payment" | "generated_claim";
field: string;
value: string | number | boolean | null;
contributesTo: "status" | "payment_dispatch" | "generated_claim_use";
}[];
};
eligibility: {
result:
| "allowed"
| "blocked"
| "requires_revalidation"
| "requires_review"
| "requires_confirmation";
blockerCodes: readonly string[];
source: "product" | "policy" | "checkout" | "payment" | "operator" | "combined";
};
evidenceRefs: readonly {
type: string;
id: string;
hash: string;
hashAlgorithm: "sha256";
}[];
nextSafeActions: readonly {
action: string;
owner: "system" | "operator" | "buyer" | "merchant";
reasonCode: string;
}[];
};
The full envelope also carries subject, actor, input references, freshness dependencies and horizons, authority, checkout, payment, and generated-claim projection state. inputDependencyHash covers decisionId, the requested surface and action, actor, subject, input references, rule-set identity, evaluation time, evidence pins, and freshness dependencies. resultHash covers the computed eligibility, authority, checkout, payment, generated-claim, freshness, basis, and next-action result. decisionHash binds the contract and schema to those two hashes.
The canonical basis keeps the result and its explanation together. Every code in basis.reasonCodes must appear in at least one basis.components[].code, and every component code must appear in the reason set. Several components may explain the same reason at different causal boundaries, but no reason can exist without a component.
The canonical result also needs an explicit precedence rule. A required hard block in authority, checkout, payment, or generated-claim use must dominate softer review, confirmation, or revalidation states. Contradictory sections—such as allowed with blocker codes, a valid checkout carrying checkout blockers, or payment dispatch attempted while payment authority is blocked—should be rejected rather than normalized into a plausible envelope.
When actor authority or checkout validity prevents payment evaluation, the payment section should remain not_evaluated with no payment-specific blockers. The upstream reasons remain in their owning sections and in the decision basis.
The authenticator states whether the envelope is protected by an Ed25519 digital signature, an HMAC-SHA-256 message-authentication code, or an explicitly unsigned local-development variant. The core eligibility idea remains simple: result, reason, evidence, and next safe action.
For the Travel Backpack, the prepare_checkout envelope can say:
basis.status = blocked
basis.reasonCodes = eligibility_blocked, invalid_checkout_state, missing_return_policy, stale_inventory
eligibility.result = blocked
checkout.validForRequestedAction = false
payment.paymentDispatchAttempted = false
nextSafeActions = revalidate inventory, attach return policy
The envelope can still pin this result to its inputs and freshness horizon, but the reader-facing decision remains easy to follow.
That structure prevents three common mistakes.
First, the feed cannot turn active and in_stock into checkout readiness.
Second, the adapter cannot treat an authenticated actor as evidence that the action itself is valid.
Third, the payment layer cannot treat a payment artifact as permission to bypass stale inventory or missing policy coverage.
Eligibility stays explicit, but it does not pretend to answer every question by itself.
Eligibility is not authority
Eligibility and authority are often confused.
They should not be.
Eligibility answers:
Is this action valid for this product under the current commercial conditions?
Authority answers:
Is this actor allowed to request this action?
Execution answers:
Can the system perform the state change now?
Those are separate checks.
For example, a product may be checkout-eligible, but a specific agent may not have permission to prepare checkout.
An agent may have a valid payment mandate, but the product may not be checkout-eligible because inventory is stale.
A checkout session may be eligible and authorized, but execution may fail because the state changed between validation and transition.
The separation looks like this:
Commercial truth
↓
Eligibility
↓
Authority
↓
Execution
↓
State transition
A command should pass through each layer.
type CommerceCommandReadiness = {
eligibility: ActionEligibilityDecision;
authority: AuthorityDecision;
execution: ExecutionReadiness;
executable: boolean;
};
type AuthorityDecision = {
actorId: string;
requestedAction: AgentCommerceDecisionAction;
allowed: boolean;
reason?: string;
scopes: string[];
};
type ExecutionReadiness = {
currentState: string;
transitionAllowed: boolean;
reason?: string;
};
This prevents one decision from doing too much work.
A product being eligible does not mean the actor is authorized.
An actor being authorized does not mean the product is eligible.
Both being true does not guarantee execution if the state transition is invalid.
This distinction becomes especially important for checkout and delegated payment.
Eligibility should consume commercial truth, not rebuild it
The eligibility layer should not rediscover product facts from raw catalog data. That would duplicate logic and create inconsistent behavior.
Instead, eligibility should consume a truth snapshot.
type AgentCommerceCommercialTruthSnapshot = {
productId: string;
truthRef: string;
facts: {
identity: "known" | "missing" | "stale" | "conflicting";
price: "known" | "missing" | "stale" | "conflicting";
inventory: "known" | "missing" | "stale" | "conflicting";
policyCoverage: "known" | "missing" | "stale" | "conflicting";
generatedClaims: "known" | "missing" | "REVIEW_REQUIRED" | "conflicting";
};
};
Eligibility then applies action-specific requirements.
type ActionRequirement = {
requestedAction: AgentCommerceDecisionAction;
requiredFacts: Array<keyof AgentCommerceCommercialTruthSnapshot["facts"]>;
blockedStatuses: Array<
"missing" | "stale" | "conflicting" | "REVIEW_REQUIRED"
>;
};
Example:
const actionRequirements: ActionRequirement[] = [
{
requestedAction: "discover",
requiredFacts: ["identity"],
blockedStatuses: ["missing", "conflicting"]
},
{
requestedAction: "compare",
requiredFacts: ["identity", "price"],
blockedStatuses: ["missing", "conflicting"]
},
{
requestedAction: "quote_policy",
requiredFacts: ["policyCoverage"],
blockedStatuses: ["missing", "stale", "conflicting"]
},
{
requestedAction: "prepare_checkout",
requiredFacts: ["price", "inventory", "policyCoverage"],
blockedStatuses: ["missing", "stale", "conflicting"]
}
];
The boundary is clean:
Commercial truth says:
Inventory is stale.
Eligibility says:
Stale inventory blocks checkout preparation.
Authority says:
This actor may or may not request checkout preparation.
Execution says:
The checkout state transition can or cannot happen now.
Each layer has a narrower responsibility.
Context matters
Product eligibility is rarely global.
A product may be eligible in one region and blocked in another. A shipping policy may exist for the EU but not for the US. A return policy may apply to consumers but not business buyers. A payment method may depend on currency. A marketplace channel may have stricter requirements than the merchant’s own storefront.
Eligibility should therefore include context.
type EligibilityContext = {
region?: string;
currency?: string;
buyerType?: "consumer" | "business";
channel?: "storefront" | "agent" | "marketplace";
actorType?: "human" | "agent";
};
The same product can produce different decisions:
Context: EU consumer, agent channel
discover allowed
compare allowed
quote_policy blocked: return policy missing
prepare_checkout blocked: inventory stale
Context: US consumer, agent channel
discover allowed
compare allowed
quote_policy blocked: return policy missing
prepare_checkout blocked: inventory stale, shipping policy unknown
Context: business buyer, agent channel
discover allowed
compare allowed
quote_policy blocked: business return terms missing
prepare_checkout blocked: business policy coverage incomplete
A global available flag cannot express this.
Context-aware eligibility is more complex, but it avoids overexposing products in cases where the platform lacks the facts required for a specific buyer or market.
Eligibility should support degradation
Eligibility is not only a gate. It is also a way to degrade safely.
The product does not need to disappear from every agent surface because one high-risk action is blocked.
For the travel backpack, the platform can expose a capability profile:
discover allowed
compare allowed
quote_policy blocked
add_to_cart requires_revalidation
prepare_checkout blocked
delegate_payment blocked
This tells the agent and the operator exactly where the product stands.
A product with incomplete return-policy coverage may still be discoverable. A product with stale inventory may still be comparable. A product with an unreviewed generated description may still be shown if the agent uses approved catalog copy instead.
This is better than two extreme alternatives:
Too permissive:
Show the product as fully available even though checkout is unsafe.
Too conservative:
Remove the product entirely even though low-risk actions are still valid.
Action-specific eligibility allows the platform to preserve safe capabilities while blocking unsafe ones.
That is a more useful operational model.
Decision results should be more than allowed or blocked
Not every negative decision should be represented as blocked.
Some decisions require revalidation. Some require human review. Some require additional authority. Some are warnings rather than blockers.
A useful result type might be:
type EligibilityResult =
| "allowed"
| "blocked"
| "requires_revalidation"
| "requires_review"
| "requires_confirmation";
For example:
discover
result: allowed
warning: generated description pending review, using approved catalog summary
add_to_cart
result: requires_revalidation
reason: inventory signal is stale
quote_policy
result: blocked
reason: return-policy coverage is missing
prepare_checkout
result: blocked
reason: inventory stale and policy coverage missing
This makes the system more expressive.
A product does not have to move directly from allowed to blocked. It can require revalidation, review, or warning-aware handling depending on the action.
Those result labels should stay attached to the action and context that produced them. A warning on discovery is not the same as a warning on checkout.
Rule identity should be explicit
Eligibility rules change.
A platform may decide that stale inventory is acceptable for cart insertion but not checkout. Later, high-demand categories may require fresh inventory even before cart insertion. A policy team may require return-policy coverage before comparison in certain regions. A marketplace channel may require stricter policy completeness than the storefront.
If eligibility decisions are stored, cached, published, or audited, the platform needs to know which rule set produced them.
type EligibilityRuleSetRef = {
ruleSetVersion: string;
ruleSetRef: `ruleset:sha256:${string}`;
ruleSetHash: string;
effectiveFrom: string;
changeRef?: string;
};
A decision should reference both the human-readable rule-set version and the content-addressed identity:
type EligibilityDecisionMetadata = {
truthRef: string;
ruleSet: EligibilityRuleSetRef;
evaluatedAt: string;
};
This helps distinguish two cases:
Case 1:
The product became blocked because inventory changed from fresh to stale.
Case 2:
The product became blocked because the eligibility rule changed.
Both are valid, but they mean different things operationally.
The first case may require inventory revalidation. The second may require merchant remediation, policy updates, or feed republishing.
Without traceable decisions, these cases become harder to explain.
Eligibility and feeds
An agent-facing product feed should not publish raw catalog availability as if it were action eligibility.
A feed item can include a compact action summary:
type AgentCommerceProductRecord = {
id: string;
title: string;
price: {
amount: number;
currency: string;
lastVerifiedAt: string;
};
actions: Partial<Record<
AgentCommerceDecisionAction,
"allowed" | "blocked" | "requires_revalidation" | "requires_review" | "requires_confirmation"
>>;
truthRef: string;
decisionHash: string;
validUntil: string | null;
publishedAt: string;
};
For the travel backpack:
{
"id": "bag_travel_42",
"title": "Travel Backpack",
"price": {
"amount": 129,
"currency": "EUR",
"lastVerifiedAt": "2026-07-06T10:12:00.000Z"
},
"actions": {
"discover": "allowed",
"compare": "allowed",
"quote_policy": "blocked",
"add_to_cart": "requires_revalidation",
"prepare_checkout": "blocked",
"delegate_payment": "blocked",
"show_generated_claim": "requires_review",
"explain": "allowed"
},
"truthRef": "truth:travel-backpack:2026-07-06",
"decisionHash": "1111111111111111111111111111111111111111111111111111111111111111",
"validUntil": "2026-07-06T10:15:00.000Z",
"publishedAt": "2026-07-06T10:12:00.000Z"
}
The feed does not need to expose every internal rule. It should expose enough for external systems to avoid unsafe assumptions.
The full blocker details can live behind a product-detail endpoint or eligibility endpoint.
This keeps the feed compact while preserving explainability.
Eligibility and protocol adapters
Protocol adapters should not invent eligibility rules.
This is a common source of drift.
One adapter calculates product visibility from catalog status. Another adapter calculates checkout readiness from inventory. The feed publisher uses a different rule. The admin UI shows a different status. The checkout service performs final validation again.
The result is inconsistent behavior.
A safer structure is:
Protocol request
↓
Protocol adapter
↓
Internal action mapping
↓
Eligibility service
↓
Domain decision
↓
Protocol-specific response
The adapter maps the external request to an internal action.
type ProtocolActionMapping = {
protocol: "acp" | "ucp" | "mcp" | "ap2" | "internal_feed" | "admin";
externalAction: string;
internalAction: AgentCommerceDecisionAction;
};
The internal eligibility service owns the decision.
The adapter only translates the result into the protocol-specific shape.
This matters because different protocols may ask similar questions with different language:
Can this product be purchased?
Can checkout be prepared?
Is this item eligible for agent checkout?
Can this offer be fulfilled?
Is delegated payment supported?
The platform should not answer each of those questions with separate business logic.
It should map them to internal actions and evaluate those actions consistently.
Eligibility and checkout
Checkout should revalidate product action status, but it should not be the first place where readiness is understood.
If eligibility exists only inside checkout, then discovery, comparison, feeds, protocol adapters, and admin workflows cannot explain product action status before checkout starts.
A better structure is:
Commercial truth
↓
Action eligibility
↓
Checkout preparation
↓
Checkout state transition
Checkout preparation can consume eligibility decisions:
type CheckoutPreparationInput = {
cartId: string;
productDecisions: ActionEligibilityDecision[];
};
If any product is not checkout-eligible, checkout preparation can return structured blockers:
type CheckoutPreparationBlocker = {
productId: string;
code: string;
message: string;
nextAction?: string;
};
This does not remove the need for final checkout validation. Prices and inventory should still be revalidated at the transition boundary.
But it prevents checkout from becoming the only part of the system that knows whether a product is commercially ready.
Eligibility gives the rest of the platform a way to reason before checkout begins.
Eligibility and delegated payment
Delegated payment is not just a higher level of product availability.
It depends on several layers.
Commercial truth
↓
Product eligibility
↓
Valid checkout state
↓
Payment authority
↓
Payment execution
A product being checkout-eligible does not mean an agent can trigger payment.
Delegated payment also requires mandate boundaries, amount limits, currency match, merchant binding, cart snapshot integrity, expiry, revocation checks, and possibly human confirmation.
A simplified readiness model might look like this:
type DelegatedPaymentReadiness = {
productEligibilityPassed: boolean;
checkoutAuthorityEstablished: boolean;
paymentMandatePresent: boolean;
cartSnapshotMatches: boolean;
amountWithinMandate: boolean;
humanConfirmationSatisfied: boolean;
};
This prevents a dangerous shortcut:
Product is available → agent can pay
That shortcut should not exist.
The correct chain is narrower and safer:
Product facts are valid.
The product is eligible for checkout.
Checkout validation passes.
The actor has payment authority.
The payment command is executable.
Each statement belongs to a different layer.
Eligibility should produce operator tasks
Eligibility decisions should not only serve agents. They should also serve operators.
When many products fail the same eligibility check, the platform should turn those failures into grouped remediation tasks.
For example:
type EligibilityIssueGroup = {
requestedAction: AgentCommerceDecisionAction;
blockerCode: string;
productIds: string[];
impact: string;
nextAction: string;
};
Operator-facing examples:
18 products are discoverable but not checkout-ready.
Reason:
Inventory freshness is stale.
Impact:
Agents can show these products, but checkout preparation is blocked.
Next action:
Revalidate inventory from the warehouse source.
Another example:
7 products are blocked from policy quotation.
Reason:
Return-policy coverage is missing.
Impact:
Agents should not quote return terms for these products.
Next action:
Attach or approve a return policy for the Travel Bags category.
This is where eligibility becomes operationally useful.
A generic validation error says something is wrong. An eligibility issue explains which commercial capability is affected.
That is the difference between debug output and an operator workflow.
Avoid boolean explosion
A common response to weak availability modeling is to add more booleans.
type Product = {
active: boolean;
inStock: boolean;
agentVisible: boolean;
comparable: boolean;
policyQuotable: boolean;
eligibleForCheckoutStart: boolean;
paymentReady: boolean;
};
This is better than one boolean, but it still has problems.
Booleans do not explain themselves. They do not include blockers, context, evidence, rule identities, or next actions.
If eligibleForCheckoutStart is false, the platform still needs to answer:
Is the price stale?
Is inventory stale?
Is policy coverage missing?
Is the product blocked in this region?
Is generated content pending review?
Did the rule identity change?
Is checkout blocked only for agents or also for humans?
Booleans are useful as summaries, especially in feeds and dashboards. They should not be the authoritative model.
A better rule is:
Use booleans for projections.
Use shared decision envelopes for commercial meaning.
For example:
type AgentCommerceProductActionSnapshot = {
productId: string;
decisions: ActionEligibilityDecision[];
summary: {
discoverable: boolean;
comparable: boolean;
policyQuotable: boolean;
checkoutEligible: boolean;
};
};
The summary is convenient. The decisions are authoritative.
Determinism matters
Eligibility decisions should be deterministic for the same inputs.
Same truth reference
+ same context
+ same rule identity
= same eligibility decision
This matters for caching, debugging, audit trails, feed publication, and agent interactions.
If an agent sees a product as checkout-eligible in a feed, then another endpoint says it is blocked, the platform needs to explain why. A valid explanation might be that a new truth reference was generated or a rule changed. An invalid explanation is that different services used different hidden logic.
A deterministic eligibility decision should record:
Product ID
Truth reference
Context
Rule set reference
Decision result
Blockers
Evidence references
Evaluation timestamp
At the envelope boundary, deterministic behavior is stronger than repeating the same business result. The same normalized input, including the same evaluatedAt, should produce identical inputDependencyHash, resultHash, and decisionHash values. Object keys and set-like collections such as blocker codes, evidence references, freshness dependencies, allowed uses, and next safe actions should be normalized before hashing so ordering alone does not create drift. Valid date-time inputs should be normalized to one UTC representation before they enter the hash.
This does not mean decisions never change. They should change when facts change, context changes, rules change, or the evaluation time intentionally changes.
The point is that change should be explainable and reproducible.
Where this model can go wrong
Action eligibility improves precision, but it introduces its own risks.
1. Eligibility can become a god service
If every product, policy, checkout, payment, protocol, and admin rule gets pushed into one eligibility service, the service becomes too broad. It starts owning decisions that belong elsewhere.
A useful boundary is:
Eligibility decides whether a product/action is valid.
Authority decides whether an actor may request it.
Checkout decides whether a state transition can occur.
Payment decides whether financial authority exists.
2. Too many blocker codes can create noise
Structured blockers are useful, but a large uncontrolled list becomes hard to operate. Blocker codes should be stable, documented, and grouped by business impact.
3. Context can make caching harder
Context-aware decisions are more correct, but they are harder to cache. A decision for an EU consumer may not apply to a US buyer or business account.
4. Feed summaries can become stale
If action summaries are published to a feed, they need to reference the truth and eligibility decisions they came from. Otherwise, external systems may rely on stale action eligibility.
5. Protocol mappings can drift
If external protocol actions are mapped inconsistently to internal actions, eligibility decisions may appear inconsistent even if the domain service is correct.
6. The model can become too conservative
If every incomplete fact blocks every action, the platform may remove useful low-risk capabilities. Discovery and comparison should not always require the same evidence as checkout or payment.
7. The model can become too permissive
If high-risk actions reuse low-risk rules, agents may be allowed to proceed without enough evidence. Checkout and delegated payment should have stricter requirements than discovery.
These failure modes do not make action eligibility a bad idea. They define the engineering constraints around it.
Practical tests for action eligibility
Eligibility should be tested through a small number of semantic scenarios, with response-shape assertions used only where they clarify the decision.
The Travel Backpack scenario is enough to catch the most dangerous drift:
Product: Travel Backpack
Price: fresh
Inventory: stale
Return policy: missing
Payment artifact: present
Expected meaning:
discover allowed
compare allowed
quote_policy blocked: missing_return_policy
add_to_cart requires_revalidation: stale_inventory
prepare_checkout blocked: stale_inventory, missing_return_policy
delegate_payment blocked because checkout is not valid; upstream blockers remain visible in checkout and the decision basis
payment authority not_evaluated; no payment-specific blockers
payment dispatch false
show_generated_claim action result requires_review; canonical claim status inherited_refusal; inheritedRefusalCount: 1
explain allowed from the decision basis
The same blocker-code meaning should survive projection into the feed, tool adapter, checkout path, and admin or support view. The tests should also assert exact reason/component reconciliation and repeat the same normalized input to confirm stable hashes. They should mutate decisionId to confirm identity tampering is detected. They should also reject contradictory allowed sections and any soft eligibility result that would otherwise hide a hard authority, checkout, or payment block.
The goal is to make one important semantic test hard to bypass: an adapter should not be able to quietly convert a blocked decision into success.
The main tradeoff
Action-specific eligibility adds complexity.
It introduces action vocabularies, rule sets, context objects, decision envelopes, blocker codes, evidence references, rule identities, and tests.
That is more work than a single available flag.
For a small storefront, this may be unnecessary. If the only user is a human browsing a simple catalog, a basic availability model may be enough.
Agent-ready commerce changes the threshold.
Once external agents can discover products, compare options, quote policies, prepare checkout, or act within delegated authority, raw availability becomes too weak as the platform contract.
The tradeoff is between simplicity and precision.
A single availability flag is simpler, but it hides risk.
Action-specific eligibility is more complex, but it makes the system explainable, safer, and more useful to agents, protocol adapters, checkout services, payment flows, and operators.
Design principles
A practical action eligibility model should follow several principles.
1. Name the actions
Do not rely on generic availability. Define the actions agents may perform.
2. Separate low-risk and high-risk actions
Discovery, comparison, policy quotation, checkout, and delegated payment should not share one rule.
3. Base eligibility on commercial truth
Eligibility should consume source-backed facts. It should not rediscover product state independently.
4. Return structured decisions
A decision should include result, blockers, warnings, evidence, context, rule identity, and evaluation time.
5. Keep eligibility separate from authority
Eligibility says whether the product/action is valid. Authority says whether the actor may request it.
6. Keep eligibility separate from execution
A product may be eligible, but the requested state transition may still be invalid at execution time.
7. Support context
Region, buyer type, channel, currency, and actor type can all affect eligibility.
8. Name the rule set
Eligibility decisions should record which rule set produced the result.
9. Use booleans only as projections
Boolean flags are useful for summaries, but decision objects should be authoritative.
10. Feed operator workflows
Blocked decisions should produce actionable tasks with business impact and next actions.
Conclusion
“Available” is too broad for agent-ready commerce.
A product can be visible but not comparable. Comparable but not policy-quotable. Policy-quotable but not checkout-ready. Checkout-ready but not eligible for delegated payment.
Those distinctions matter because agents need explicit action boundaries.
The platform should therefore expose action-specific eligibility decisions instead of relying on one product availability flag.
Commercial truth says what is known.
Eligibility says which actions are valid.
Authority says who may request them.
Execution performs the state change.
This separation keeps the system easier to reason about. It also creates better interfaces for agents, protocol adapters, feeds, checkout services, payment flows, and operator tools.
Next in the series
Part 4 will move from product actions to policy structure:
Agent-Ready Commerce, Part 4: Making Policies Machine-Readable
That article will examine how returns, shipping, warranty, cancellation, regional restrictions, and policy conflicts can be modeled as structured facts instead of free-text pages that agents are expected to interpret.
About the author
Written by Dimitrios S. Sfyris, Founder & Software Architect at AspectSoft.
AspectSoft designs and develops custom software platforms, e-commerce systems, SaaS infrastructure, integrations, analytics tools, and practical digital products.
You can also follow the AspectSoft LinkedIn page for updates on software platforms, commerce systems, AI tooling, and developer-focused products.
Top comments (2)
Excellent perspective. I especially like the distinction between commercial truth, eligibility, authority, and execution. Replacing a single available flag with action-specific eligibility makes AI agent interactions far more reliable, explainable, and safer, particularly for checkout and delegated payment workflows. Great architectural insight.
Glad it resonated. Appreciate you taking the time to read it thoughtfully, and for the generous feedback.