@absol-labs/agent 0.9.1 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +184 -26
- package/dist/capability/invocation-capability.d.ts +18 -6
- package/dist/capability/invocation-capability.d.ts.map +1 -1
- package/dist/capability/invocation-capability.js.map +1 -1
- package/dist/cli/amounts.d.ts +26 -0
- package/dist/cli/amounts.d.ts.map +1 -0
- package/dist/cli/amounts.js +71 -0
- package/dist/cli/amounts.js.map +1 -0
- package/dist/cli/args.d.ts +32 -0
- package/dist/cli/args.d.ts.map +1 -0
- package/dist/cli/args.js +119 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/attesters.d.ts +51 -0
- package/dist/cli/attesters.d.ts.map +1 -0
- package/dist/cli/attesters.js +58 -0
- package/dist/cli/attesters.js.map +1 -0
- package/dist/cli/bin.d.ts +3 -0
- package/dist/cli/bin.d.ts.map +1 -0
- package/dist/cli/bin.js +5 -0
- package/dist/cli/bin.js.map +1 -0
- package/dist/cli/commands/claim.d.ts +13 -0
- package/dist/cli/commands/claim.d.ts.map +1 -0
- package/dist/cli/commands/claim.js +102 -0
- package/dist/cli/commands/claim.js.map +1 -0
- package/dist/cli/commands/close.d.ts +3 -0
- package/dist/cli/commands/close.d.ts.map +1 -0
- package/dist/cli/commands/close.js +55 -0
- package/dist/cli/commands/close.js.map +1 -0
- package/dist/cli/commands/discover.d.ts +3 -0
- package/dist/cli/commands/discover.d.ts.map +1 -0
- package/dist/cli/commands/discover.js +183 -0
- package/dist/cli/commands/discover.js.map +1 -0
- package/dist/cli/commands/invoke.d.ts +3 -0
- package/dist/cli/commands/invoke.d.ts.map +1 -0
- package/dist/cli/commands/invoke.js +142 -0
- package/dist/cli/commands/invoke.js.map +1 -0
- package/dist/cli/commands/mcp.d.ts +3 -0
- package/dist/cli/commands/mcp.d.ts.map +1 -0
- package/dist/cli/commands/mcp.js +113 -0
- package/dist/cli/commands/mcp.js.map +1 -0
- package/dist/cli/commands/open.d.ts +3 -0
- package/dist/cli/commands/open.d.ts.map +1 -0
- package/dist/cli/commands/open.js +331 -0
- package/dist/cli/commands/open.js.map +1 -0
- package/dist/cli/commands/reclaim-unverified.d.ts +3 -0
- package/dist/cli/commands/reclaim-unverified.d.ts.map +1 -0
- package/dist/cli/commands/reclaim-unverified.js +172 -0
- package/dist/cli/commands/reclaim-unverified.js.map +1 -0
- package/dist/cli/commands/reclaim.d.ts +33 -0
- package/dist/cli/commands/reclaim.d.ts.map +1 -0
- package/dist/cli/commands/reclaim.js +308 -0
- package/dist/cli/commands/reclaim.js.map +1 -0
- package/dist/cli/commands/status.d.ts +3 -0
- package/dist/cli/commands/status.d.ts.map +1 -0
- package/dist/cli/commands/status.js +43 -0
- package/dist/cli/commands/status.js.map +1 -0
- package/dist/cli/commands/streams.d.ts +3 -0
- package/dist/cli/commands/streams.d.ts.map +1 -0
- package/dist/cli/commands/streams.js +257 -0
- package/dist/cli/commands/streams.js.map +1 -0
- package/dist/cli/commands/watch.d.ts +3 -0
- package/dist/cli/commands/watch.d.ts.map +1 -0
- package/dist/cli/commands/watch.js +55 -0
- package/dist/cli/commands/watch.js.map +1 -0
- package/dist/cli/config.d.ts +61 -0
- package/dist/cli/config.d.ts.map +1 -0
- package/dist/cli/config.js +140 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/context.d.ts +29 -0
- package/dist/cli/context.d.ts.map +1 -0
- package/dist/cli/context.js +2 -0
- package/dist/cli/context.js.map +1 -0
- package/dist/cli/deps.d.ts +191 -0
- package/dist/cli/deps.d.ts.map +1 -0
- package/dist/cli/deps.js +277 -0
- package/dist/cli/deps.js.map +1 -0
- package/dist/cli/descriptor.d.ts +61 -0
- package/dist/cli/descriptor.d.ts.map +1 -0
- package/dist/cli/descriptor.js +78 -0
- package/dist/cli/descriptor.js.map +1 -0
- package/dist/cli/errors.d.ts +48 -0
- package/dist/cli/errors.d.ts.map +1 -0
- package/dist/cli/errors.js +129 -0
- package/dist/cli/errors.js.map +1 -0
- package/dist/cli/escape-hatch.d.ts +50 -0
- package/dist/cli/escape-hatch.d.ts.map +1 -0
- package/dist/cli/escape-hatch.js +45 -0
- package/dist/cli/escape-hatch.js.map +1 -0
- package/dist/cli/index.d.ts +8 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +7 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/listings.d.ts +15 -0
- package/dist/cli/listings.d.ts.map +1 -0
- package/dist/cli/listings.js +56 -0
- package/dist/cli/listings.js.map +1 -0
- package/dist/cli/mandate.d.ts +66 -0
- package/dist/cli/mandate.d.ts.map +1 -0
- package/dist/cli/mandate.js +61 -0
- package/dist/cli/mandate.js.map +1 -0
- package/dist/cli/output.d.ts +33 -0
- package/dist/cli/output.d.ts.map +1 -0
- package/dist/cli/output.js +34 -0
- package/dist/cli/output.js.map +1 -0
- package/dist/cli/preflight.d.ts +38 -0
- package/dist/cli/preflight.d.ts.map +1 -0
- package/dist/cli/preflight.js +72 -0
- package/dist/cli/preflight.js.map +1 -0
- package/dist/cli/privy.d.ts +83 -0
- package/dist/cli/privy.d.ts.map +1 -0
- package/dist/cli/privy.js +230 -0
- package/dist/cli/privy.js.map +1 -0
- package/dist/cli/progress.d.ts +36 -0
- package/dist/cli/progress.d.ts.map +1 -0
- package/dist/cli/progress.js +40 -0
- package/dist/cli/progress.js.map +1 -0
- package/dist/cli/run.d.ts +25 -0
- package/dist/cli/run.d.ts.map +1 -0
- package/dist/cli/run.js +173 -0
- package/dist/cli/run.js.map +1 -0
- package/dist/cli/scan-range.d.ts +55 -0
- package/dist/cli/scan-range.d.ts.map +1 -0
- package/dist/cli/scan-range.js +57 -0
- package/dist/cli/scan-range.js.map +1 -0
- package/dist/cli/spend-policy.d.ts +60 -0
- package/dist/cli/spend-policy.d.ts.map +1 -0
- package/dist/cli/spend-policy.js +65 -0
- package/dist/cli/spend-policy.js.map +1 -0
- package/dist/cli/status.d.ts +90 -0
- package/dist/cli/status.d.ts.map +1 -0
- package/dist/cli/status.js +252 -0
- package/dist/cli/status.js.map +1 -0
- package/dist/cli/timeout.d.ts +30 -0
- package/dist/cli/timeout.d.ts.map +1 -0
- package/dist/cli/timeout.js +55 -0
- package/dist/cli/timeout.js.map +1 -0
- package/dist/cli/version.d.ts +12 -0
- package/dist/cli/version.d.ts.map +1 -0
- package/dist/cli/version.js +22 -0
- package/dist/cli/version.js.map +1 -0
- package/dist/cli/wallet.d.ts +164 -0
- package/dist/cli/wallet.d.ts.map +1 -0
- package/dist/cli/wallet.js +234 -0
- package/dist/cli/wallet.js.map +1 -0
- package/dist/frameworks/crewai.d.ts +10 -0
- package/dist/frameworks/crewai.d.ts.map +1 -1
- package/dist/frameworks/crewai.js +12 -2
- package/dist/frameworks/crewai.js.map +1 -1
- package/dist/gateway/caller-auth-gateway.d.ts +38 -3
- package/dist/gateway/caller-auth-gateway.d.ts.map +1 -1
- package/dist/gateway/caller-auth-gateway.js +25 -1
- package/dist/gateway/caller-auth-gateway.js.map +1 -1
- package/dist/gateway/http-server.d.ts +53 -0
- package/dist/gateway/http-server.d.ts.map +1 -1
- package/dist/gateway/http-server.js +189 -4
- package/dist/gateway/http-server.js.map +1 -1
- package/dist/gateway/server-entry.d.ts +1 -0
- package/dist/gateway/server-entry.js +1 -0
- package/dist/gateway/server-entry.js.map +1 -1
- package/dist/gateway/x402-mode.d.ts +123 -0
- package/dist/gateway/x402-mode.d.ts.map +1 -0
- package/dist/gateway/x402-mode.js +212 -0
- package/dist/gateway/x402-mode.js.map +1 -0
- package/dist/index.d.ts +6 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -3
- package/dist/index.js.map +1 -1
- package/dist/mandates/env.d.ts +3 -2
- package/dist/mandates/env.d.ts.map +1 -1
- package/dist/mandates/env.js +3 -2
- package/dist/mandates/env.js.map +1 -1
- package/dist/mcp/http-server.d.ts +1 -0
- package/dist/mcp/http-server.js +1 -0
- package/dist/mcp/http-server.js.map +1 -1
- package/dist/mcp/server.d.ts +49 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +533 -25
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/service-invoker.d.ts +97 -0
- package/dist/mcp/service-invoker.d.ts.map +1 -0
- package/dist/mcp/service-invoker.js +167 -0
- package/dist/mcp/service-invoker.js.map +1 -0
- package/dist/mcp/stdio.d.ts +1 -0
- package/dist/mcp/stdio.js +1 -0
- package/dist/mcp/stdio.js.map +1 -1
- package/dist/sdk/client.d.ts +25 -1
- package/dist/sdk/client.d.ts.map +1 -1
- package/dist/sdk/client.js +20 -1
- package/dist/sdk/client.js.map +1 -1
- package/dist/sdk/invoke.d.ts +19 -4
- package/dist/sdk/invoke.d.ts.map +1 -1
- package/dist/sdk/invoke.js +69 -5
- package/dist/sdk/invoke.js.map +1 -1
- package/dist/x402/delivery-oracle.d.ts +108 -0
- package/dist/x402/delivery-oracle.d.ts.map +1 -0
- package/dist/x402/delivery-oracle.js +174 -0
- package/dist/x402/delivery-oracle.js.map +1 -0
- package/dist/x402/facilitator-server-entry.d.ts +3 -0
- package/dist/x402/facilitator-server-entry.d.ts.map +1 -0
- package/dist/x402/facilitator-server-entry.js +27 -0
- package/dist/x402/facilitator-server-entry.js.map +1 -0
- package/dist/x402/facilitator-server.d.ts +73 -0
- package/dist/x402/facilitator-server.d.ts.map +1 -0
- package/dist/x402/facilitator-server.js +305 -0
- package/dist/x402/facilitator-server.js.map +1 -0
- package/dist/x402/facilitator.d.ts +69 -17
- package/dist/x402/facilitator.d.ts.map +1 -1
- package/dist/x402/facilitator.js +76 -33
- package/dist/x402/facilitator.js.map +1 -1
- package/dist/x402/payment-requirements.d.ts +106 -0
- package/dist/x402/payment-requirements.d.ts.map +1 -0
- package/dist/x402/payment-requirements.js +138 -0
- package/dist/x402/payment-requirements.js.map +1 -0
- package/dist/x402/relayer.d.ts +41 -0
- package/dist/x402/relayer.d.ts.map +1 -0
- package/dist/x402/relayer.js +64 -0
- package/dist/x402/relayer.js.map +1 -0
- package/dist/x402/resource-index.d.ts +75 -0
- package/dist/x402/resource-index.d.ts.map +1 -0
- package/dist/x402/resource-index.js +126 -0
- package/dist/x402/resource-index.js.map +1 -0
- package/dist/x402/verified-delivery-facilitator.d.ts +285 -0
- package/dist/x402/verified-delivery-facilitator.d.ts.map +1 -0
- package/dist/x402/verified-delivery-facilitator.js +423 -0
- package/dist/x402/verified-delivery-facilitator.js.map +1 -0
- package/dist/zktls/t2-delivery-proof.d.ts +6 -6
- package/docs/agent-layer.md +14 -9
- package/docs/cli.md +705 -0
- package/docs/crewai.md +6 -2
- package/docs/eliza.md +6 -1
- package/docs/mcp-hosted.md +7 -5
- package/docs/mcp-reference.md +175 -0
- package/docs/quickstart.md +36 -9
- package/docs/threat-model.md +4 -3
- package/docs/x402.md +648 -0
- package/package.json +33 -10
- package/src/capability/invocation-capability.ts +17 -1
- package/src/cli/amounts.ts +102 -0
- package/src/cli/args.ts +150 -0
- package/src/cli/attesters.ts +86 -0
- package/src/cli/bin.ts +5 -0
- package/src/cli/commands/claim.ts +126 -0
- package/src/cli/commands/close.ts +69 -0
- package/src/cli/commands/discover.ts +228 -0
- package/src/cli/commands/invoke.ts +178 -0
- package/src/cli/commands/mcp.ts +135 -0
- package/src/cli/commands/open.ts +397 -0
- package/src/cli/commands/reclaim-unverified.ts +218 -0
- package/src/cli/commands/reclaim.ts +377 -0
- package/src/cli/commands/status.ts +56 -0
- package/src/cli/commands/streams.ts +344 -0
- package/src/cli/commands/watch.ts +70 -0
- package/src/cli/config.ts +208 -0
- package/src/cli/context.ts +30 -0
- package/src/cli/deps.ts +528 -0
- package/src/cli/descriptor.ts +133 -0
- package/src/cli/errors.ts +162 -0
- package/src/cli/escape-hatch.ts +92 -0
- package/src/cli/index.ts +27 -0
- package/src/cli/listings.ts +77 -0
- package/src/cli/mandate.ts +126 -0
- package/src/cli/output.ts +63 -0
- package/src/cli/preflight.ts +88 -0
- package/src/cli/privy.ts +396 -0
- package/src/cli/progress.ts +75 -0
- package/src/cli/run.ts +209 -0
- package/src/cli/scan-range.ts +76 -0
- package/src/cli/spend-policy.ts +139 -0
- package/src/cli/status.ts +381 -0
- package/src/cli/timeout.ts +64 -0
- package/src/cli/version.ts +21 -0
- package/src/cli/wallet.ts +372 -0
- package/src/frameworks/crewai.ts +13 -2
- package/src/gateway/caller-auth-gateway.ts +77 -6
- package/src/gateway/http-server.ts +280 -6
- package/src/gateway/server-entry.ts +1 -0
- package/src/gateway/x402-mode.ts +350 -0
- package/src/index.ts +40 -5
- package/src/mandates/env.ts +3 -2
- package/src/mcp/http-server.ts +1 -0
- package/src/mcp/server.ts +706 -30
- package/src/mcp/service-invoker.ts +285 -0
- package/src/mcp/stdio.ts +1 -0
- package/src/sdk/client.ts +33 -1
- package/src/sdk/invoke.ts +90 -12
- package/src/x402/delivery-oracle.ts +285 -0
- package/src/x402/facilitator-server-entry.ts +35 -0
- package/src/x402/facilitator-server.ts +445 -0
- package/src/x402/facilitator.ts +125 -46
- package/src/x402/payment-requirements.ts +231 -0
- package/src/x402/relayer.ts +122 -0
- package/src/x402/resource-index.ts +228 -0
- package/src/x402/verified-delivery-facilitator.ts +744 -0
|
@@ -0,0 +1,744 @@
|
|
|
1
|
+
import { getAddress } from "viem";
|
|
2
|
+
import {
|
|
3
|
+
PaymentPayloadSchema,
|
|
4
|
+
PaymentRequirementsSchema,
|
|
5
|
+
type PaymentPayload,
|
|
6
|
+
type PaymentRequirements,
|
|
7
|
+
type SettleResponse,
|
|
8
|
+
type VerifyResponse,
|
|
9
|
+
} from "x402/types";
|
|
10
|
+
|
|
11
|
+
import type {
|
|
12
|
+
DeliveryAssessment,
|
|
13
|
+
DeliveryVerdict,
|
|
14
|
+
DeliveryVerdictReason,
|
|
15
|
+
MetrikDeliveryOracle,
|
|
16
|
+
} from "./delivery-oracle.js";
|
|
17
|
+
import type { MetrikResourceIndex } from "./resource-index.js";
|
|
18
|
+
import {
|
|
19
|
+
X402_NETWORK_BY_CHAIN_ID,
|
|
20
|
+
X402_USDC_BY_CHAIN_ID,
|
|
21
|
+
X402_VERSION,
|
|
22
|
+
} from "./payment-requirements.js";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Metrik's x402 facilitator: a fully conformant `exact` facilitator that ALSO
|
|
26
|
+
* refuses to settle payment for a service its own oracle says is not delivering.
|
|
27
|
+
*
|
|
28
|
+
* ## Why this is spec-legal
|
|
29
|
+
*
|
|
30
|
+
* `VerifyResponse` is `{ isValid: boolean, invalidReason?, payer? }`. Nothing in
|
|
31
|
+
* the x402 specification constrains WHY a facilitator answers `false`. A
|
|
32
|
+
* facilitator is the party a resource server delegates its "should this payment
|
|
33
|
+
* go through" decision to; Metrik simply knows one more thing than every other
|
|
34
|
+
* facilitator, because it runs a prober.
|
|
35
|
+
*
|
|
36
|
+
* ## What this is NOT
|
|
37
|
+
*
|
|
38
|
+
* **This is strictly weaker buyer protection than a Metrik escrow stream.** A
|
|
39
|
+
* stream holds the buyer's money and releases it only for intervals that
|
|
40
|
+
* verified, so money already spent on a service that stopped delivering is
|
|
41
|
+
* recoverable. This gate holds nothing. It can only prevent a payment that has
|
|
42
|
+
* not happened yet; it can never recover one that has. An `exact` payment that
|
|
43
|
+
* Metrik lets through is gone, exactly as it would be through any other
|
|
44
|
+
* facilitator.
|
|
45
|
+
*
|
|
46
|
+
* And `isValid: true` from this facilitator is NOT an endorsement. It means only
|
|
47
|
+
* "the payment is well-formed and Metrik has no fresh evidence that this service
|
|
48
|
+
* is failing". The gate subtracts; it never adds.
|
|
49
|
+
*
|
|
50
|
+
* ## Scope: only services Metrik actually tracks
|
|
51
|
+
*
|
|
52
|
+
* A resource that maps to no Metrik listing gets the ordinary facilitator
|
|
53
|
+
* answer, unchanged. Refusing payments for services Metrik has no opinion about
|
|
54
|
+
* would not be caution, it would be a broken facilitator.
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
/** What the delivery gate did to a payment. */
|
|
58
|
+
export type MetrikGateOutcome =
|
|
59
|
+
/** The resource is not a Metrik listing — ordinary facilitator behaviour. */
|
|
60
|
+
| "not-applicable"
|
|
61
|
+
/** Fresh evidence the service is delivering; payment allowed to proceed. */
|
|
62
|
+
| "passed"
|
|
63
|
+
/** Fresh evidence the service is failing; payment refused. */
|
|
64
|
+
| "refused"
|
|
65
|
+
/** No fresh evidence; `indeterminatePolicy` decided. */
|
|
66
|
+
| "indeterminate"
|
|
67
|
+
/** The resource's payee is not the operator who signed the listing. */
|
|
68
|
+
| "payee-mismatch"
|
|
69
|
+
/** Metrik could not tell whether the resource is one of its listings. */
|
|
70
|
+
| "registry-unavailable";
|
|
71
|
+
|
|
72
|
+
/** The delivery gate's own verdict, before settlement policy is applied. */
|
|
73
|
+
export interface MetrikGateVerdict {
|
|
74
|
+
readonly gate: MetrikGateOutcome;
|
|
75
|
+
/** Human-readable explanation. See "Where the explanation goes" below. */
|
|
76
|
+
readonly detail: string;
|
|
77
|
+
readonly serviceRef?: string;
|
|
78
|
+
readonly deliveryVerdict?: DeliveryVerdict;
|
|
79
|
+
readonly deliveryReason?: DeliveryVerdictReason;
|
|
80
|
+
readonly lastCheckedAt?: number | null;
|
|
81
|
+
readonly verdictAgeSeconds?: number | null;
|
|
82
|
+
readonly freshnessWindowSeconds?: number;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface MetrikGateAnnotation extends MetrikGateVerdict {
|
|
86
|
+
/**
|
|
87
|
+
* Whether THIS facilitator would broadcast this payment if asked.
|
|
88
|
+
*
|
|
89
|
+
* Distinct from `isValid`, and deliberately reported on `/verify`: a payment
|
|
90
|
+
* can be perfectly valid and still be one this relayer will not spend its own
|
|
91
|
+
* gas on (see {@link SettlementRefusalReason}). Reporting it up front lets a
|
|
92
|
+
* resource server refuse BEFORE it executes the request, instead of doing the
|
|
93
|
+
* work and then failing to get paid for it.
|
|
94
|
+
*/
|
|
95
|
+
readonly settleable: boolean;
|
|
96
|
+
/** Why `settleable` is false. Absent when it is true. */
|
|
97
|
+
readonly settlementRefusal?: SettlementRefusalReason;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export const SETTLEMENT_REFUSAL_DETAIL: Readonly<
|
|
101
|
+
Record<SettlementRefusalReason, string>
|
|
102
|
+
> = Object.freeze({
|
|
103
|
+
"asset-not-allowed":
|
|
104
|
+
"the settlement asset is not on this facilitator's allowlist. The relayer signs a call to " +
|
|
105
|
+
"`asset`, so an unrestricted asset would let a caller choose which contract the operator's " +
|
|
106
|
+
"key calls and pays gas for.",
|
|
107
|
+
"network-mismatch":
|
|
108
|
+
"the payload or the requirements name a chain this facilitator does not settle on",
|
|
109
|
+
"zero-amount":
|
|
110
|
+
"a zero-value settlement moves nothing and would only burn the relayer's gas",
|
|
111
|
+
"resource-not-tracked":
|
|
112
|
+
"this relayer only pays gas for resources Metrik tracks, and this resource did not resolve " +
|
|
113
|
+
"to exactly one signed Metrik listing. The payment is not refused — any other facilitator " +
|
|
114
|
+
"can settle it; Metrik is declining to spend its own gas on it.",
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* A `VerifyResponse` with Metrik's reasoning attached.
|
|
119
|
+
*
|
|
120
|
+
* ### Where the explanation goes
|
|
121
|
+
*
|
|
122
|
+
* `invalidReason` is a CLOSED enum in the x402 spec and there is no member for
|
|
123
|
+
* "this service is not delivering". Inventing one would break every conformant
|
|
124
|
+
* client that parses the field. So the enum carries the closest honest member
|
|
125
|
+
* (see {@link DELIVERY_REFUSAL_REASON}) and the real explanation travels in a
|
|
126
|
+
* `metrik` object alongside it.
|
|
127
|
+
*
|
|
128
|
+
* `VerifyResponseSchema` is a `strip` (not `strict`) Zod object, so this extra
|
|
129
|
+
* key is spec-tolerated: a conformant client parses the response successfully
|
|
130
|
+
* and simply drops `metrik`. A client that wants the reason can read it off the
|
|
131
|
+
* raw body. Nothing about the standard fields changes.
|
|
132
|
+
*/
|
|
133
|
+
export type MetrikVerifyResponse = VerifyResponse & {
|
|
134
|
+
readonly metrik: MetrikGateAnnotation;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
export type MetrikSettleResponse = SettleResponse & {
|
|
138
|
+
readonly metrik: MetrikGateAnnotation;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Why this facilitator will not BROADCAST a payment, independent of whether the
|
|
143
|
+
* payment is valid.
|
|
144
|
+
*
|
|
145
|
+
* `/settle` makes the operator's relayer sign and broadcast a call to
|
|
146
|
+
* `paymentRequirements.asset` and pay the gas for it. Every field of
|
|
147
|
+
* `PaymentRequirements` arrives in the request body, and `PaymentRequirementsSchema`
|
|
148
|
+
* accepts ANY `asset` address, ANY `payTo`, `maxAmountRequired: "0"`, and any
|
|
149
|
+
* valid-URL `resource`. The library's `verify` only checks the payload against
|
|
150
|
+
* those same caller-supplied requirements — signature recovers to
|
|
151
|
+
* `authorization.from`, `to === payTo`, window valid, and
|
|
152
|
+
* `balanceOf(from) >= maxAmountRequired` **on the caller-supplied asset**. An
|
|
153
|
+
* attacker signing from their own key to their own address satisfies all of it.
|
|
154
|
+
*
|
|
155
|
+
* Without the checks below, `/settle` is a permissionless "make the relayer
|
|
156
|
+
* broadcast a call to a contract of my choosing" endpoint: deploy something
|
|
157
|
+
* whose `balanceOf` returns a huge number and whose `transferWithAuthorization`
|
|
158
|
+
* burns gas, and drain the relayer until legitimate settlement stops. No funds
|
|
159
|
+
* are stolen — but it is the operator's money, and it induces the operator's key
|
|
160
|
+
* to sign transactions to contracts they never chose.
|
|
161
|
+
*
|
|
162
|
+
* The asset allowlist is the one that removes the arbitrary-contract-call
|
|
163
|
+
* primitive outright; the rest are defence in depth.
|
|
164
|
+
*/
|
|
165
|
+
export type SettlementRefusalReason =
|
|
166
|
+
/** `asset` is not an operator-approved settlement token. */
|
|
167
|
+
| "asset-not-allowed"
|
|
168
|
+
/** The payload or the requirements name a chain this facilitator does not settle. */
|
|
169
|
+
| "network-mismatch"
|
|
170
|
+
/** A zero-value settlement moves nothing and only burns the relayer's gas. */
|
|
171
|
+
| "zero-amount"
|
|
172
|
+
/** The relayer only pays gas for resources Metrik actually tracks. */
|
|
173
|
+
| "resource-not-tracked";
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Which resources this facilitator will spend the relayer's gas on.
|
|
177
|
+
*
|
|
178
|
+
* `tracked-resources` (default) is a deliberate ASYMMETRY with the delivery
|
|
179
|
+
* gate's allow-by-default policy, and the asymmetry is the point: an OPINION
|
|
180
|
+
* about a service costs nothing and should be given freely, so `/verify` stays
|
|
181
|
+
* universal and fails open. BROADCASTING costs the operator real gas, so
|
|
182
|
+
* `/settle` fails closed and is scoped to Metrik's own marketplace.
|
|
183
|
+
*
|
|
184
|
+
* This is not a censorship lever. Metrik declining to spend its own gas does
|
|
185
|
+
* not stop anyone being paid — any other facilitator (or the payer's own
|
|
186
|
+
* relayer) can settle the same payment.
|
|
187
|
+
*
|
|
188
|
+
* `any-resource` is for an operator deliberately running a general-purpose
|
|
189
|
+
* facilitator on a relayer they are content to fund for anyone's traffic. The
|
|
190
|
+
* asset allowlist still applies.
|
|
191
|
+
*/
|
|
192
|
+
export type SettlementScope = "tracked-resources" | "any-resource";
|
|
193
|
+
|
|
194
|
+
export const DEFAULT_SETTLEMENT_SCOPE: SettlementScope = "tracked-resources";
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* The `invalidReason` used when the delivery gate refuses.
|
|
198
|
+
*
|
|
199
|
+
* The enum has no member for "the resource is not delivering", so the choice is
|
|
200
|
+
* between members that are all imperfect. Every payer-side member
|
|
201
|
+
* (`invalid_payload`, `invalid_exact_evm_payload_signature`,
|
|
202
|
+
* `insufficient_funds`) would be an active lie: it sends the developer to debug
|
|
203
|
+
* a wallet that is working correctly. `unexpected_verify_error` is also false —
|
|
204
|
+
* this refusal is deliberate and expected, not an error.
|
|
205
|
+
*
|
|
206
|
+
* `invalid_payment_requirements` is the only member that points at the
|
|
207
|
+
* REQUIREMENTS side of the exchange, which is exactly where the problem is:
|
|
208
|
+
* Metrik will not settle a payment quoted for THIS resource to THIS payee right
|
|
209
|
+
* now. It is the honest member, and the `metrik.detail` string carries the rest.
|
|
210
|
+
*/
|
|
211
|
+
export const DELIVERY_REFUSAL_REASON = "invalid_payment_requirements" as const;
|
|
212
|
+
|
|
213
|
+
/** What to do when Metrik has no fresh delivery evidence. */
|
|
214
|
+
export type IndeterminatePolicy = "allow" | "deny";
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Default: `allow`.
|
|
218
|
+
*
|
|
219
|
+
* The standing Metrik rule is buyer-favouring, and it still is — but it applies
|
|
220
|
+
* where Metrik HOLDS the buyer's money and must decide whether to release it.
|
|
221
|
+
* Here Metrik holds nothing, so refusing does not protect a buyer's funds; it
|
|
222
|
+
* only denies them a service they asked to buy, on a rail that works fine
|
|
223
|
+
* without Metrik. Four things make `allow` the right default:
|
|
224
|
+
*
|
|
225
|
+
* 1. **Absent evidence is the normal case, not the exception.** The oracle's
|
|
226
|
+
* `/services` projection is built from ACTIVE STREAMS. A service with no live
|
|
227
|
+
* Metrik stream has no row at all — on the live oracle today that is every
|
|
228
|
+
* service. Failing closed would refuse 100% of payments, which is a broken
|
|
229
|
+
* facilitator rather than a careful one.
|
|
230
|
+
* 2. **Fail-closed is a censorship lever.** If "oracle down" meant "no payments",
|
|
231
|
+
* anyone who could degrade Metrik's oracle could block payments to a
|
|
232
|
+
* competitor listed on it. `allow` removes that lever entirely.
|
|
233
|
+
* 3. **The harms are asymmetric.** A wrong refusal denies a service outright. A
|
|
234
|
+
* wrong allowance passes one `exact` payment, at a price the buyer already
|
|
235
|
+
* agreed to, which by construction is one call's worth.
|
|
236
|
+
* 4. **A pass never claims anything.** `isValid: true` with
|
|
237
|
+
* `gate: "indeterminate"` says "no fresh evidence", and the annotation says
|
|
238
|
+
* so in the response. Nobody has to infer an endorsement that was not made.
|
|
239
|
+
*
|
|
240
|
+
* Set `indeterminatePolicy: "deny"` for a deployment that would rather refuse
|
|
241
|
+
* than proceed without positive fresh evidence. That is a real, supported
|
|
242
|
+
* choice — it is just not a safe default.
|
|
243
|
+
*/
|
|
244
|
+
export const DEFAULT_INDETERMINATE_POLICY: IndeterminatePolicy = "allow";
|
|
245
|
+
|
|
246
|
+
/** The payment-validity half of the facilitator (signature, amount, funds, window). */
|
|
247
|
+
export interface PaymentVerifier {
|
|
248
|
+
verify(
|
|
249
|
+
payload: PaymentPayload,
|
|
250
|
+
requirements: PaymentRequirements,
|
|
251
|
+
): Promise<VerifyResponse>;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** The broadcast half. Requires a funded relayer wallet. */
|
|
255
|
+
export interface PaymentSettler {
|
|
256
|
+
settle(
|
|
257
|
+
payload: PaymentPayload,
|
|
258
|
+
requirements: PaymentRequirements,
|
|
259
|
+
): Promise<SettleResponse>;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export class RelayerNotConfiguredError extends Error {
|
|
263
|
+
constructor() {
|
|
264
|
+
super(
|
|
265
|
+
"this facilitator has no settlement relayer configured, so /settle cannot broadcast. " +
|
|
266
|
+
"Set METRIK_X402_RELAYER_PRIVATE_KEY (and METRIK_X402_RPC_URL) to enable settlement. " +
|
|
267
|
+
"Refusing rather than silently returning a fake success.",
|
|
268
|
+
);
|
|
269
|
+
this.name = "RelayerNotConfiguredError";
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export interface MetrikVerifiedDeliveryFacilitatorOptions {
|
|
274
|
+
readonly paymentVerifier: PaymentVerifier;
|
|
275
|
+
/** Omit to run a verify-only facilitator: `/settle` then fails loudly. */
|
|
276
|
+
readonly paymentSettler?: PaymentSettler;
|
|
277
|
+
readonly resourceIndex: MetrikResourceIndex;
|
|
278
|
+
readonly deliveryOracle: MetrikDeliveryOracle;
|
|
279
|
+
/** Default {@link DEFAULT_INDETERMINATE_POLICY}. */
|
|
280
|
+
readonly indeterminatePolicy?: IndeterminatePolicy;
|
|
281
|
+
/** Chain this facilitator settles on. Default `84532` (Base Sepolia). */
|
|
282
|
+
readonly chainId?: number;
|
|
283
|
+
/**
|
|
284
|
+
* Token contracts the relayer may be asked to call. Defaults to the single
|
|
285
|
+
* canonical USDC for `chainId`. An empty array is rejected: a settling
|
|
286
|
+
* facilitator with no allowed asset can never settle anything, and silently
|
|
287
|
+
* accepting that would hide a misconfiguration.
|
|
288
|
+
*
|
|
289
|
+
* THIS IS THE CONTROL that stops `/settle` being an arbitrary-contract-call
|
|
290
|
+
* primitive. See {@link SettlementRefusalReason}.
|
|
291
|
+
*/
|
|
292
|
+
readonly settlementAssetAllowlist?: readonly string[];
|
|
293
|
+
/** Default {@link DEFAULT_SETTLEMENT_SCOPE}. */
|
|
294
|
+
readonly settlementScope?: SettlementScope;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export interface FacilitatorSupported {
|
|
298
|
+
readonly x402Version: number;
|
|
299
|
+
readonly kind: readonly {
|
|
300
|
+
readonly scheme: string;
|
|
301
|
+
readonly networkId: string;
|
|
302
|
+
readonly extra: object;
|
|
303
|
+
}[];
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export class MetrikVerifiedDeliveryFacilitator {
|
|
307
|
+
private readonly paymentVerifier: PaymentVerifier;
|
|
308
|
+
private readonly paymentSettler: PaymentSettler | undefined;
|
|
309
|
+
private readonly resourceIndex: MetrikResourceIndex;
|
|
310
|
+
private readonly deliveryOracle: MetrikDeliveryOracle;
|
|
311
|
+
private readonly indeterminatePolicy: IndeterminatePolicy;
|
|
312
|
+
private readonly network: string;
|
|
313
|
+
private readonly settlementAssets: ReadonlySet<string>;
|
|
314
|
+
private readonly settlementScope: SettlementScope;
|
|
315
|
+
|
|
316
|
+
constructor(options: MetrikVerifiedDeliveryFacilitatorOptions) {
|
|
317
|
+
const chainId = options.chainId ?? 84532;
|
|
318
|
+
const network = X402_NETWORK_BY_CHAIN_ID[chainId];
|
|
319
|
+
if (network === undefined) {
|
|
320
|
+
throw new Error(
|
|
321
|
+
`x402 v1 has no network name for chain ${chainId} (Metrik settles on Base: 8453, 84532)`,
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
this.paymentVerifier = options.paymentVerifier;
|
|
325
|
+
this.paymentSettler = options.paymentSettler;
|
|
326
|
+
this.resourceIndex = options.resourceIndex;
|
|
327
|
+
this.deliveryOracle = options.deliveryOracle;
|
|
328
|
+
this.indeterminatePolicy =
|
|
329
|
+
options.indeterminatePolicy ?? DEFAULT_INDETERMINATE_POLICY;
|
|
330
|
+
this.network = network;
|
|
331
|
+
|
|
332
|
+
const defaultAsset = X402_USDC_BY_CHAIN_ID[chainId]?.address;
|
|
333
|
+
const allowlist: readonly string[] =
|
|
334
|
+
options.settlementAssetAllowlist ??
|
|
335
|
+
(defaultAsset === undefined ? [] : [defaultAsset]);
|
|
336
|
+
if (allowlist.length === 0) {
|
|
337
|
+
throw new Error(
|
|
338
|
+
`no settlement asset allowlist for chain ${chainId} and none supplied. ` +
|
|
339
|
+
"Refusing to construct a facilitator that would either settle nothing or, " +
|
|
340
|
+
"worse, broadcast to a caller-chosen contract.",
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
const normalized = new Set<string>();
|
|
344
|
+
for (const candidate of allowlist) {
|
|
345
|
+
let checksummed: string;
|
|
346
|
+
try {
|
|
347
|
+
checksummed = getAddress(candidate as `0x${string}`);
|
|
348
|
+
} catch {
|
|
349
|
+
throw new Error(
|
|
350
|
+
`settlementAssetAllowlist entry is not an EVM address: ${candidate}`,
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
normalized.add(checksummed);
|
|
354
|
+
}
|
|
355
|
+
this.settlementAssets = normalized;
|
|
356
|
+
this.settlementScope = options.settlementScope ?? DEFAULT_SETTLEMENT_SCOPE;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** The assets this relayer may be asked to call, for logging at startup. */
|
|
360
|
+
get allowedSettlementAssets(): readonly string[] {
|
|
361
|
+
return [...this.settlementAssets];
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
supported(): FacilitatorSupported {
|
|
365
|
+
return {
|
|
366
|
+
x402Version: X402_VERSION,
|
|
367
|
+
kind: [{ scheme: "exact", networkId: this.network, extra: {} }],
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** `POST /verify`. Takes an unvalidated body; never throws on bad input. */
|
|
372
|
+
async verify(body: unknown): Promise<MetrikVerifyResponse> {
|
|
373
|
+
const parsed = parseFacilitatorRequest(body);
|
|
374
|
+
if (parsed === undefined) {
|
|
375
|
+
return {
|
|
376
|
+
isValid: false,
|
|
377
|
+
invalidReason: "invalid_payload",
|
|
378
|
+
metrik: {
|
|
379
|
+
gate: "not-applicable",
|
|
380
|
+
settleable: false,
|
|
381
|
+
detail:
|
|
382
|
+
"request body is not a valid x402 {paymentPayload, paymentRequirements} pair",
|
|
383
|
+
},
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
return this.evaluate(parsed.paymentPayload, parsed.paymentRequirements);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* `POST /settle`. Re-runs the full gate before broadcasting: a facilitator
|
|
391
|
+
* must never settle something it would not verify, and `/settle` is reachable
|
|
392
|
+
* without `/verify` ever having been called.
|
|
393
|
+
*/
|
|
394
|
+
async settle(body: unknown): Promise<MetrikSettleResponse> {
|
|
395
|
+
const parsed = parseFacilitatorRequest(body);
|
|
396
|
+
if (parsed === undefined) {
|
|
397
|
+
return {
|
|
398
|
+
success: false,
|
|
399
|
+
errorReason: "invalid_payload",
|
|
400
|
+
transaction: "",
|
|
401
|
+
network: this.network as SettleResponse["network"],
|
|
402
|
+
metrik: {
|
|
403
|
+
gate: "not-applicable",
|
|
404
|
+
settleable: false,
|
|
405
|
+
detail:
|
|
406
|
+
"request body is not a valid x402 {paymentPayload, paymentRequirements} pair",
|
|
407
|
+
},
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Settlement constraints that need no network round trip run FIRST, so a
|
|
412
|
+
// hostile body never reaches the RPC, the registry or the oracle — let
|
|
413
|
+
// alone the relayer.
|
|
414
|
+
const preflight = this.settlementPreflight(
|
|
415
|
+
parsed.paymentPayload,
|
|
416
|
+
parsed.paymentRequirements,
|
|
417
|
+
);
|
|
418
|
+
if (preflight !== undefined) {
|
|
419
|
+
return this.refuseSettlement(parsed.paymentPayload, preflight, undefined);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const gate = await this.evaluate(
|
|
423
|
+
parsed.paymentPayload,
|
|
424
|
+
parsed.paymentRequirements,
|
|
425
|
+
);
|
|
426
|
+
if (!gate.isValid) {
|
|
427
|
+
return {
|
|
428
|
+
success: false,
|
|
429
|
+
...(gate.invalidReason === undefined
|
|
430
|
+
? {}
|
|
431
|
+
: { errorReason: gate.invalidReason }),
|
|
432
|
+
transaction: "",
|
|
433
|
+
network: parsed.paymentPayload.network,
|
|
434
|
+
...(gate.payer === undefined ? {} : { payer: gate.payer }),
|
|
435
|
+
metrik: gate.metrik,
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (!gate.metrik.settleable) {
|
|
440
|
+
return this.refuseSettlement(
|
|
441
|
+
parsed.paymentPayload,
|
|
442
|
+
gate.metrik.settlementRefusal ?? "resource-not-tracked",
|
|
443
|
+
gate.metrik,
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (this.paymentSettler === undefined) {
|
|
448
|
+
throw new RelayerNotConfiguredError();
|
|
449
|
+
}
|
|
450
|
+
const settled = await this.paymentSettler.settle(
|
|
451
|
+
parsed.paymentPayload,
|
|
452
|
+
parsed.paymentRequirements,
|
|
453
|
+
);
|
|
454
|
+
return { ...settled, metrik: gate.metrik };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Settlement constraints checkable from the request alone — no RPC, no
|
|
459
|
+
* registry, no oracle. Returns the refusal, or `undefined` if the request
|
|
460
|
+
* clears them.
|
|
461
|
+
*/
|
|
462
|
+
private settlementPreflight(
|
|
463
|
+
payload: PaymentPayload,
|
|
464
|
+
requirements: PaymentRequirements,
|
|
465
|
+
): SettlementRefusalReason | undefined {
|
|
466
|
+
if (
|
|
467
|
+
payload.network !== this.network ||
|
|
468
|
+
requirements.network !== this.network
|
|
469
|
+
) {
|
|
470
|
+
return "network-mismatch";
|
|
471
|
+
}
|
|
472
|
+
// THE control. Without it the relayer signs a call to whatever contract
|
|
473
|
+
// address the caller puts here. See `SettlementRefusalReason`.
|
|
474
|
+
let asset: string;
|
|
475
|
+
try {
|
|
476
|
+
asset = getAddress(requirements.asset);
|
|
477
|
+
} catch {
|
|
478
|
+
return "asset-not-allowed";
|
|
479
|
+
}
|
|
480
|
+
if (!this.settlementAssets.has(asset)) {
|
|
481
|
+
return "asset-not-allowed";
|
|
482
|
+
}
|
|
483
|
+
let amount: bigint;
|
|
484
|
+
try {
|
|
485
|
+
amount = BigInt(requirements.maxAmountRequired);
|
|
486
|
+
} catch {
|
|
487
|
+
return "zero-amount";
|
|
488
|
+
}
|
|
489
|
+
if (amount <= 0n) {
|
|
490
|
+
return "zero-amount";
|
|
491
|
+
}
|
|
492
|
+
return undefined;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/** Builds the standard-shaped refusal for a settlement the relayer declines. */
|
|
496
|
+
private refuseSettlement(
|
|
497
|
+
payload: PaymentPayload,
|
|
498
|
+
reason: SettlementRefusalReason,
|
|
499
|
+
gate: MetrikGateVerdict | undefined,
|
|
500
|
+
): MetrikSettleResponse {
|
|
501
|
+
return {
|
|
502
|
+
success: false,
|
|
503
|
+
errorReason:
|
|
504
|
+
reason === "network-mismatch"
|
|
505
|
+
? "invalid_network"
|
|
506
|
+
: "invalid_payment_requirements",
|
|
507
|
+
transaction: "",
|
|
508
|
+
network: this.network as SettleResponse["network"],
|
|
509
|
+
metrik: {
|
|
510
|
+
...(gate ?? { gate: "not-applicable" as const, detail: "" }),
|
|
511
|
+
settleable: false,
|
|
512
|
+
settlementRefusal: reason,
|
|
513
|
+
detail: SETTLEMENT_REFUSAL_DETAIL[reason],
|
|
514
|
+
},
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
private async evaluate(
|
|
519
|
+
payload: PaymentPayload,
|
|
520
|
+
requirements: PaymentRequirements,
|
|
521
|
+
): Promise<MetrikVerifyResponse> {
|
|
522
|
+
const evaluated = await this.evaluateGate(payload, requirements);
|
|
523
|
+
// `settleable` is computed once, here, from the same facts `/settle`
|
|
524
|
+
// enforces — so a resource server reading `/verify` learns BEFORE it does
|
|
525
|
+
// any work whether this relayer will actually broadcast.
|
|
526
|
+
const refusal =
|
|
527
|
+
this.settlementPreflight(payload, requirements) ??
|
|
528
|
+
(evaluated.isValid
|
|
529
|
+
? this.scopeRefusal(evaluated.metrik.gate)
|
|
530
|
+
: "resource-not-tracked");
|
|
531
|
+
return {
|
|
532
|
+
...evaluated,
|
|
533
|
+
metrik: {
|
|
534
|
+
...evaluated.metrik,
|
|
535
|
+
settleable: refusal === undefined,
|
|
536
|
+
...(refusal === undefined ? {} : { settlementRefusal: refusal }),
|
|
537
|
+
},
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/** Does this gate outcome name a resource the relayer will pay gas for? */
|
|
542
|
+
private scopeRefusal(
|
|
543
|
+
gate: MetrikGateOutcome,
|
|
544
|
+
): SettlementRefusalReason | undefined {
|
|
545
|
+
if (this.settlementScope === "any-resource") {
|
|
546
|
+
return undefined;
|
|
547
|
+
}
|
|
548
|
+
// Only an origin that resolved to exactly one signed listing counts.
|
|
549
|
+
// "not-applicable" (not ours), "ambiguous" (two claimants) and
|
|
550
|
+
// "registry-unavailable" (cannot tell) are all "we do not know this is
|
|
551
|
+
// ours", and the relayer does not spend gas on that.
|
|
552
|
+
return gate === "passed" ||
|
|
553
|
+
gate === "indeterminate" ||
|
|
554
|
+
gate === "refused" ||
|
|
555
|
+
gate === "payee-mismatch"
|
|
556
|
+
? undefined
|
|
557
|
+
: "resource-not-tracked";
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
private async evaluateGate(
|
|
561
|
+
payload: PaymentPayload,
|
|
562
|
+
requirements: PaymentRequirements,
|
|
563
|
+
): Promise<VerifyResponse & { readonly metrik: MetrikGateVerdict }> {
|
|
564
|
+
// 1. Ordinary payment validity first. A malformed or unfunded payment is
|
|
565
|
+
// rejected on its own merits, and never reaches — or leaks — the
|
|
566
|
+
// delivery record of a service.
|
|
567
|
+
const payment = await this.paymentVerifier.verify(payload, requirements);
|
|
568
|
+
if (!payment.isValid) {
|
|
569
|
+
return {
|
|
570
|
+
...payment,
|
|
571
|
+
metrik: {
|
|
572
|
+
gate: "not-applicable",
|
|
573
|
+
detail:
|
|
574
|
+
"payment rejected on ordinary x402 grounds; the Metrik delivery gate did not run",
|
|
575
|
+
},
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// 2. Is this resource one of ours? Resolved from Metrik's own verified
|
|
580
|
+
// registry view, keyed on the resource URL's origin.
|
|
581
|
+
const lookup = await this.resourceIndex.lookup(requirements.resource);
|
|
582
|
+
if (lookup.status === "registry-unavailable") {
|
|
583
|
+
return this.applyIndeterminate(payment, {
|
|
584
|
+
gate: "registry-unavailable",
|
|
585
|
+
detail:
|
|
586
|
+
"Metrik could not read its service registry, so it cannot tell whether this resource is one it tracks",
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
if (lookup.status !== "resolved" || lookup.resource === undefined) {
|
|
590
|
+
// Not a Metrik listing. Behave exactly like any other facilitator.
|
|
591
|
+
return {
|
|
592
|
+
...payment,
|
|
593
|
+
metrik: {
|
|
594
|
+
gate: "not-applicable",
|
|
595
|
+
detail:
|
|
596
|
+
"resource is not a Metrik-listed service; Metrik has no delivery opinion and applied no gate",
|
|
597
|
+
},
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
const resolved = lookup.resource;
|
|
601
|
+
|
|
602
|
+
// 3. The payee must be the operator who SIGNED the listing. This is the one
|
|
603
|
+
// check that protects the payer rather than the seller: without it,
|
|
604
|
+
// anyone could quote a Metrik-listed resource and redirect its payment to
|
|
605
|
+
// their own address.
|
|
606
|
+
if (!addressesEqual(requirements.payTo, resolved.operator)) {
|
|
607
|
+
return {
|
|
608
|
+
isValid: false,
|
|
609
|
+
invalidReason: "invalid_exact_evm_payload_recipient_mismatch",
|
|
610
|
+
...(payment.payer === undefined ? {} : { payer: payment.payer }),
|
|
611
|
+
metrik: {
|
|
612
|
+
gate: "payee-mismatch",
|
|
613
|
+
serviceRef: resolved.serviceRef,
|
|
614
|
+
detail:
|
|
615
|
+
`payTo ${requirements.payTo} is not the operator ${resolved.operator} who signed the listing ` +
|
|
616
|
+
`for ${resolved.matchedOrigin}; refusing to settle a payment for a Metrik-listed resource to a different address`,
|
|
617
|
+
},
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// 4. The differentiator.
|
|
622
|
+
const assessment = await this.deliveryOracle.verdictFor(
|
|
623
|
+
resolved.serviceRef,
|
|
624
|
+
);
|
|
625
|
+
if (assessment.verdict === "failing") {
|
|
626
|
+
return {
|
|
627
|
+
isValid: false,
|
|
628
|
+
invalidReason: DELIVERY_REFUSAL_REASON,
|
|
629
|
+
...(payment.payer === undefined ? {} : { payer: payment.payer }),
|
|
630
|
+
metrik: {
|
|
631
|
+
gate: "refused",
|
|
632
|
+
detail:
|
|
633
|
+
`Metrik's oracle reports this service as FAILING its delivery checks ` +
|
|
634
|
+
`(verdict recorded ${assessment.ageSeconds ?? "?"}s ago, within the ` +
|
|
635
|
+
`${assessment.freshnessWindowSeconds}s freshness window). The payment itself is valid; ` +
|
|
636
|
+
`Metrik is refusing to settle it. This prevents a payment — it does not recover one.`,
|
|
637
|
+
...assessmentFields(assessment, resolved.serviceRef),
|
|
638
|
+
},
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
if (assessment.verdict === "indeterminate") {
|
|
642
|
+
return this.applyIndeterminate(payment, {
|
|
643
|
+
gate: "indeterminate",
|
|
644
|
+
detail:
|
|
645
|
+
`Metrik has no fresh delivery evidence for this service (${assessment.reason}; ` +
|
|
646
|
+
`freshness window ${assessment.freshnessWindowSeconds}s). ` +
|
|
647
|
+
`Policy is "${this.indeterminatePolicy}".`,
|
|
648
|
+
...assessmentFields(assessment, resolved.serviceRef),
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
return {
|
|
652
|
+
...payment,
|
|
653
|
+
metrik: {
|
|
654
|
+
gate: "passed",
|
|
655
|
+
detail:
|
|
656
|
+
`Metrik's oracle recorded a passing delivery check for this service ` +
|
|
657
|
+
`${assessment.ageSeconds ?? "?"}s ago. This is not an endorsement of output correctness — ` +
|
|
658
|
+
`Metrik verifies delivery, not correctness.`,
|
|
659
|
+
...assessmentFields(assessment, resolved.serviceRef),
|
|
660
|
+
},
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
private applyIndeterminate(
|
|
665
|
+
payment: VerifyResponse,
|
|
666
|
+
annotation: MetrikGateVerdict,
|
|
667
|
+
): VerifyResponse & { readonly metrik: MetrikGateVerdict } {
|
|
668
|
+
if (this.indeterminatePolicy === "deny") {
|
|
669
|
+
return {
|
|
670
|
+
isValid: false,
|
|
671
|
+
invalidReason: DELIVERY_REFUSAL_REASON,
|
|
672
|
+
...(payment.payer === undefined ? {} : { payer: payment.payer }),
|
|
673
|
+
metrik: {
|
|
674
|
+
...annotation,
|
|
675
|
+
detail: `${annotation.detail} Refusing because this facilitator is configured to require positive fresh evidence.`,
|
|
676
|
+
},
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
return {
|
|
680
|
+
...payment,
|
|
681
|
+
metrik: {
|
|
682
|
+
...annotation,
|
|
683
|
+
detail:
|
|
684
|
+
`${annotation.detail} Allowing: Metrik holds none of this money, so refusing would deny the ` +
|
|
685
|
+
`service without protecting anyone. "isValid: true" here asserts only that the payment is ` +
|
|
686
|
+
`well-formed — it is NOT a statement that the service is delivering.`,
|
|
687
|
+
},
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function assessmentFields(
|
|
693
|
+
assessment: DeliveryAssessment,
|
|
694
|
+
serviceRef: string,
|
|
695
|
+
): Omit<MetrikGateVerdict, "gate" | "detail"> {
|
|
696
|
+
return {
|
|
697
|
+
serviceRef,
|
|
698
|
+
deliveryVerdict: assessment.verdict,
|
|
699
|
+
deliveryReason: assessment.reason,
|
|
700
|
+
lastCheckedAt: assessment.lastCheckedAt,
|
|
701
|
+
verdictAgeSeconds: assessment.ageSeconds,
|
|
702
|
+
freshnessWindowSeconds: assessment.freshnessWindowSeconds,
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function addressesEqual(a: string, b: string): boolean {
|
|
707
|
+
try {
|
|
708
|
+
return getAddress(a) === getAddress(b);
|
|
709
|
+
} catch {
|
|
710
|
+
return false;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
interface ParsedFacilitatorRequest {
|
|
715
|
+
readonly paymentPayload: PaymentPayload;
|
|
716
|
+
readonly paymentRequirements: PaymentRequirements;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* Parses the facilitator wire body with the REAL x402 schemas. Both `/verify`
|
|
721
|
+
* and `/settle` take the same `{paymentPayload, paymentRequirements}` pair.
|
|
722
|
+
*/
|
|
723
|
+
export function parseFacilitatorRequest(
|
|
724
|
+
body: unknown,
|
|
725
|
+
): ParsedFacilitatorRequest | undefined {
|
|
726
|
+
if (typeof body !== "object" || body === null) {
|
|
727
|
+
return undefined;
|
|
728
|
+
}
|
|
729
|
+
const candidate = body as Record<string, unknown>;
|
|
730
|
+
const payload = PaymentPayloadSchema.safeParse(candidate.paymentPayload);
|
|
731
|
+
if (!payload.success) {
|
|
732
|
+
return undefined;
|
|
733
|
+
}
|
|
734
|
+
const requirements = PaymentRequirementsSchema.safeParse(
|
|
735
|
+
candidate.paymentRequirements,
|
|
736
|
+
);
|
|
737
|
+
if (!requirements.success) {
|
|
738
|
+
return undefined;
|
|
739
|
+
}
|
|
740
|
+
return {
|
|
741
|
+
paymentPayload: payload.data,
|
|
742
|
+
paymentRequirements: requirements.data,
|
|
743
|
+
};
|
|
744
|
+
}
|