Skip to main content

ChallengeEngine

The ChallengeEngine class orchestrates the full challenge lifecycle: creating challenges, verifying payments, transitioning state, and issuing credentials. It is instantiated internally by createKey0() and shared across all transports (Express, Hono, Fastify, MCP). You rarely construct a ChallengeEngine directly. Use the factory unless you need full control over wiring.

Constructor

import { ChallengeEngine } from "@key0ai/key0";

const engine = new ChallengeEngine({
  config: SellerConfig,
  store: IChallengeStore,
  seenTxStore: ISeenTxStore,
  adapter: IPaymentAdapter,
});
ParameterTypeDescription
configSellerConfigSeller configuration including wallet address, network, and plans.
storeIChallengeStorePersistent store for challenge records. Must support atomic transitions.
seenTxStoreISeenTxStoreStore for tracking used transaction hashes to prevent double-spend.
adapterIPaymentAdapterPayment adapter that verifies on-chain transfer events (e.g. X402Adapter).

Methods

requestAccess

Creates a PENDING challenge for the A2A protocol flow. Idempotent — if a challenge with the same requestId already exists, the existing challenge is returned.
requestAccess(request: AccessRequest): Promise<X402Challenge>
ParameterTypeDescription
requestAccessRequestContains requestId, planId, and resourceId.
Returns: Promise<X402Challenge> — the challenge object containing challengeId, payment amount, destination wallet, and chainId.

requestHttpAccess

Creates a PENDING challenge for the x402 HTTP flow. Auto-generates a requestId if one is not provided.
requestHttpAccess(
  requestId: string,
  planId: string,
  resourceId: string
): Promise<{ challengeId: string }>
ParameterTypeDescription
requestIdstringUnique identifier for the request.
planIdstringThe pricing plan to use for the challenge.
resourceIdstringIdentifier for the resource being accessed.
Returns: Promise<{ challengeId: string }> — the ID of the created challenge.

submitProof

Verifies a payment on-chain via adapter.verifyProof(), transitions the challenge from PENDING to PAID to DELIVERED, and issues credentials. Used by the A2A protocol flow.
submitProof(proof: PaymentProof): Promise<AccessGrant>
ParameterTypeDescription
proofPaymentProofContains challengeId and txHash of the on-chain transaction.
Returns: Promise<AccessGrant> — the grant containing the issued credential (JWT, API key, etc.) from fetchResourceCredentials.
This method performs on-chain verification. It will reject if the transaction does not match the expected amount, destination, or token contract.

processHttpPayment

Handles payment for the x402 HTTP flow. Unlike submitProof, this method skips on-chain verification because the x402 settlement layer has already verified the payment. Creates a challenge if one does not yet exist, then transitions through PAID to DELIVERED.
processHttpPayment(
  requestId: string,
  planId: string,
  resourceId: string,
  txHash: string,
  payer?: string
): Promise<AccessGrant>
ParameterTypeRequiredDescription
requestIdstringYesUnique identifier for the request.
planIdstringYesThe pricing plan used for payment.
resourceIdstringYesIdentifier for the resource being accessed.
txHashstringYesTransaction hash from the on-chain settlement.
payerstringNoWallet address of the payer, if known.
Returns: Promise<AccessGrant> — the grant containing the issued credential.

preSettlementCheck

Returns a cached AccessGrant if the challenge has already been delivered. Returns null otherwise. Use this to avoid duplicate settlement when a client retries a request that was already fulfilled.
preSettlementCheck(requestId: string): Promise<AccessGrant | null>
ParameterTypeDescription
requestIdstringThe request ID to look up.
Returns: Promise<AccessGrant | null> — the existing grant, or null if no delivered grant exists.

cancelChallenge

Transitions a challenge from PENDING to CANCELLED. Only pending challenges can be cancelled.
cancelChallenge(challengeId: string): Promise<void>
ParameterTypeDescription
challengeIdstringThe ID of the challenge to cancel.
Returns: Promise<void>

State Machine

ChallengeEngine enforces a strict state machine for every challenge. Only valid transitions are permitted; invalid transitions throw a Key0Error.
PENDING --> PAID --> DELIVERED
PENDING --> EXPIRED
PENDING --> CANCELLED
PAID --> REFUND_PENDING --> REFUNDED
PAID --> REFUND_PENDING --> REFUND_FAILED
All transitions are atomic and go through IChallengeStore.transition() to prevent race conditions under concurrent access. For the full state machine specification, see the State Machine page.

Error Handling

All methods throw Key0Error with typed error codes when a failure occurs. Common scenarios include:
Error CodeCause
CHALLENGE_NOT_FOUNDThe referenced challenge ID does not exist in the store.
INVALID_TRANSITIONThe challenge is not in a valid state for the operation.
PAYMENT_VERIFICATION_FAILEDOn-chain verification did not match expected parameters.
DUPLICATE_TXThe transaction hash has already been used for another challenge.
See the full list of error codes at Error Codes.

Usage Example

In most applications, you interact with ChallengeEngine indirectly through the framework integration. For direct usage:
import { ChallengeEngine, X402Adapter } from "@key0ai/key0";
import { RedisChallengeStore, RedisSeenTxStore } from "@key0ai/key0";

const engine = new ChallengeEngine({
  config: sellerConfig,
  store: new RedisChallengeStore(redis),
  seenTxStore: new RedisSeenTxStore(redis),
  adapter: new X402Adapter("mainnet"),
});

// A2A flow
const challenge = await engine.requestAccess({
  requestId: "req-001",
  planId: "basic",
  resourceId: "/api/data",
});

// After the client pays on-chain...
const grant = await engine.submitProof({
  challengeId: challenge.challengeId,
  txHash: "0xabc...",
});

console.log(grant.credential); // JWT or API key