@twzrd1111/clawrouter-mpp 0.1.0 → 0.3.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.
Files changed (4) hide show
  1. package/README.md +172 -15
  2. package/dist/index.d.ts +204 -27
  3. package/dist/index.js +966 -112
  4. package/package.json +16 -5
package/README.md CHANGED
@@ -3,45 +3,202 @@
3
3
  MPP (Machine Payments Protocol) payment rails for [ClawRouter](https://github.com/BlockRunAI/ClawRouter):
4
4
  pay an MPP seller over **Tempo** or **Solana** instead of x402, under ClawRouter's own spend limits.
5
5
 
6
+ ## Upgrading from 0.2.0
7
+
8
+ - Fulfillment rows now use `twzrd.mpp.fulfillment.v2`. Readers must accept the new schema to retain
9
+ `response_bytes` and `body_sha256`; these fields are null when the recorder does not read a complete
10
+ body within its 1 MiB cap.
11
+ - Credit rows carry an optional `reference` matching the error and spend record. It is a local
12
+ signed-payload hash, not a settlement signature.
13
+ - `payFetch.flush({ timeoutMs })` bounds waiting for both records and ledger persistence. A timeout
14
+ leaves the paid response and background writes intact; an omitted timeout keeps the previous
15
+ unbounded wait. Values above 2,147,483,647 ms are rejected.
16
+
6
17
  ```ts
7
18
  import { startProxy, SpendControl, FileSpendControlStorage } from "@twzrd1111/clawrouter";
8
19
  import { createMppFetch, createSolanaMppFetch } from "@twzrd1111/clawrouter-mpp";
20
+ import { createTwzrdMppIntegration, createLocalDecisionSigner } from "twzrd-x402-gate";
9
21
 
10
22
  const spend = new SpendControl({ storage: new FileSpendControlStorage() });
23
+ const twzrdMpp = createTwzrdMppIntegration({
24
+ signer: createLocalDecisionSigner(),
25
+ policy: { maxAmountUsd: "1.00" },
26
+ });
11
27
 
12
- // Tempo (needs `mppx`)
13
- const payFetch = createMppFetch(account, fetch, { spend });
14
- // or Solana (needs `@solana/pay-kit`)
15
- // const payFetch = await createSolanaMppFetch({ signer, rpcUrl, network: "devnet", spend });
28
+ // Tempo (needs `mppx`); alternatively use createSolanaMppFetch with the same hooks.
29
+ const payFetch = createMppFetch(account, fetch, { spend, integration: twzrdMpp });
30
+ const proxy = await startProxy({ spendControl: spend, payFetch /* ... */ });
16
31
 
17
- await startProxy({ spendControl: spend, payFetch /* ... */ });
32
+ // proxy.close() closes the listener, then drains the rail's pending ledger writes.
33
+ await proxy.close();
18
34
  ```
19
35
 
20
- Requires ClawRouter with the `payFetch` seam. Until that ships upstream
21
- ([BlockRunAI/ClawRouter#546](https://github.com/BlockRunAI/ClawRouter/pull/546)), install the
22
- `@twzrd1111/clawrouter` distribution: upstream ClawRouter plus that seam, nothing else, with version numbers
23
- mirroring upstream. When the seam is released upstream, switch the import and peer dependency back to
24
- `@blockrun/clawrouter`.
36
+ The supported setup uses the integration helper returned by `createTwzrdMppIntegration`.
37
+ The `twzrdMppIntegration` marker is a wiring check only; it is forgeable and does not authenticate
38
+ the origin of the hooks. Its gate receives the full MPP charge challenge before credential
39
+ creation and returns
40
+ `{ disposition: "allow" | "warn" | "block" }`; `block` and callback errors refuse payment before
41
+ signing, while `warn` proceeds. The MPP rail rejects integrations without the marker or required
42
+ hooks before loading payment SDKs.
43
+
44
+ The integration writes the pre-sign decision durably to `.twzrd/decisions.jsonl` before allowing
45
+ credential creation, then appends the projected MPP receipt to that same file. Fulfillment writes
46
+ run asynchronously so they do not hold the response stream. `payFetch.flush?.()` waits for pending
47
+ records and fsyncs the local ledger during orderly shutdown. `startProxy` calls the rail's flush hook
48
+ after closing its listener, and direct rail users must call `payFetch.flush()` themselves. It waits for
49
+ active SSE responses too, so drain or cancel them before flushing; an open stream can keep the flush
50
+ pending. `payFetch.flush({ timeoutMs })` bounds the entire wait for records and ledger persistence
51
+ (0 through 2,147,483,647 ms). At the deadline it still attempts the ledger flush in the background,
52
+ then rejects with an `AggregateError` holding an `MppFlushTimeoutError`. Its `pending` field counts
53
+ unfinished records; `ledgerPending` indicates whether ledger flushing is still in progress.
54
+ A late ledger failure is reported once: by a flush still waiting on that ledger, otherwise by the next explicit flush. (Ownership is tracked per ledger promise; an integration that returns a fresh wrapper promise for the same failure to each caller can still see it twice.) A timeout does not confirm durability.
55
+ It never cancels or reads a response body, so a late reader still gets the whole paid response,
56
+ and its record lands when that body is drained. Network contribution remains off unless separately enabled. It blocks unscored
57
+ networks by default, including Tempo mainnet and Moderato, until TWZRD has a reputation corpus for
58
+ them. The default provider also fails closed on outages; the ledger sink itself makes no network
59
+ call. An explicit `allowUnscoredNetwork: true` requires `policy.maxAmountUsd` and returns
60
+ `reputation_unchecked` as a warning; warnings proceed unless `treatWarnAsBlock` is set. The
61
+ fulfillment row omits prompts, responses, credentials, and raw receipt headers; its seller model
62
+ field is explicitly a seller claim. Network contribution remains a separate opt-in.
63
+
64
+ This integration requires `twzrd-x402-gate@0.17.0`, which is not published yet. npm's current
65
+ `0.16.2` does not include Tempo charge evaluation or the shared-ledger adapter.
66
+
67
+ Receipt-safe payment metadata is available through `readPaidCallEvidence(response)` for a paid
68
+ response from this rail. The
69
+ fulfillment hook records the decoded receipt reference, never the raw base64 receipt or payment
70
+ credential. Tempo session receipts emitted in an SSE `payment-receipt` event are captured when the
71
+ response stream completes; the stream bytes are forwarded unchanged, including the seller-reported
72
+ model label from its first JSON data event. A `valid_format` receipt means its structure, method, and
73
+ intent match the challenge; it is not an on-chain verification. `seller_reported_model_matches_request`
74
+ compares labels only and does not verify model identity. A failed local write does not change an
75
+ already-paid response; `payFetch.flush()` surfaces persistence failures to the shutdown caller.
76
+
77
+ Rows use schema `twzrd.mpp.fulfillment.v2`. `response_bytes` and `body_sha256` (lowercase hex
78
+ SHA-256) describe the complete response body as the recorder read it: a JSON body read through its
79
+ clone, or an SSE body tapped to its end. Both are `null` when the recorder did not read the body,
80
+ it exceeded 1 MiB, or the stream was cancelled or errored before its end; a partial body is never
81
+ hashed. The digest is of bytes the seller sent and is not a seller attestation.
82
+
83
+ This candidate requires `@twzrd1111/clawrouter >=0.12.309`, including deferred streaming headers.
84
+ The fork is the release target. Upstream PRs #545 and #546 are independent of this release.
85
+
86
+ For Tempo install the optional peer `mppx@~0.17.0`. For Solana install `@solana/mpp@~0.7.0`,
87
+ `mppx@~0.17.0`, and `@solana/kit@~6.10.0`. Solana now uses the official charge method
88
+ directly, replacing the pay-kit client wrapper. That wrapper's progress events omit the
89
+ challenge ID; the direct method lets policy checks and credits use the full challenge
90
+ before signing. `@solana/pay-kit` is used only by development tests as a fixture seller.
25
91
 
26
92
  ## What it guarantees
27
93
 
28
94
  - **Spend limits apply.** The amount, network, asset and payee are checked, and budget reserved, _before_
29
- anything is signed. Payee lists on Solana are checked at the challenge event and fail closed.
95
+ anything is signed. Solana checks the complete challenge before transaction construction
96
+ and pins the configured network even when no spend control is supplied.
30
97
  The proxy refuses to start a rail that does not set `enforcesSpend` while amount limits are configured.
31
98
  - **A signed payment is never paid twice.** If a call fails after a credential or transaction was signed, it
32
99
  ends in `MppPaidNoCompletionError`: the proxy answers 502 `payment_settled_no_completion` and does not
33
100
  retry or fall back. The spend ledger keeps the spend with a note, and the error carries a payment
34
101
  reference (`mpp-<16 hex>`, a hash of the signed payload) for disputes.
35
- - **Optional peers.** `mppx` and `@solana/pay-kit` are loaded on the first paid call; a missing one produces an
36
- install instruction, not a crash at import.
102
+ - **Streaming receipts.** Both factories set `deferStreamingHeaders`. The proxy holds final
103
+ headers while paying, sends informational HTTP 102 responses, and forwards the seller's
104
+ exact `Payment-Receipt` on the final streaming response. It does not fabricate a missing receipt.
105
+ Both also set `supportsNativeStreaming`: streaming chat requests retain `stream: true`
106
+ upstream and request usage chunks. Native SSE flows incrementally, including structured
107
+ tool calls, with reasoning filtered and token/cost accounting retained.
108
+ Ambiguous text-encoded calls wait for the choice to finish so recovery matches the existing
109
+ whole-message parser; structured tool deltas and ordinary text flow incrementally.
110
+ JSON-only replies use the existing simulated stream. In-flight coalesced callers receive live chunks from
111
+ one paid attempt; completed cache replays do not claim a new receipt.
112
+ HTTP 102 does not extend OpenClaw's deadline. Real content/tool deltas reset client activity
113
+ timers as the model streams. Payment/header wait and stalled output still have deadlines.
114
+ - **Optional peers.** Importing this package does not load payment SDKs. Tempo loads `mppx` on
115
+ first use; the asynchronous Solana factory loads `@solana/mpp`. Missing peers produce an
116
+ install instruction.
117
+
118
+ "Signed" is not "settled": on Solana the seller submits the transaction. Seller-side refunds
119
+ remain outside this package.
120
+
121
+ ## Replay protection at a seller
122
+
123
+ ```ts
124
+ import { SpentProofLedger, fulfillPaidProof, ReplayRejectedError } from "@twzrd1111/clawrouter-mpp";
125
+
126
+ const proofs = new SpentProofLedger(); // share across requests in this process
127
+
128
+ // First authenticate the challenge and independently verify its payment on chain.
129
+ // Pass a canonical network ID plus verified transaction and challenge IDs,
130
+ // never untrusted body fields. Example IDs: "solana:mainnet-beta", "eip155:4217".
131
+ const completion = await fulfillPaidProof({
132
+ ledger: proofs,
133
+ networkId: verifiedNetworkId,
134
+ txSignature: verifiedSignature,
135
+ challengeId: verifiedChallengeId,
136
+ upstream: () => runModel(),
137
+ });
138
+ ```
139
+
140
+ A reused transaction or challenge on the same network throws `ReplayRejectedError` with
141
+ `status: 409` and `code: "proof_replayed"` before invoking `upstream`. Network scope allows
142
+ the same signature text or challenge ID to exist independently on another chain. The
143
+ synchronous claim gives concurrent duplicates one winner. The claim remains spent if the
144
+ model fails. This helper does not verify a payment itself,
145
+ and installing a buyer rail cannot add replay protection to a remote seller.
146
+
147
+ `SpentProofLedger` keeps claims in memory, so a restart or a second process forgets spent
148
+ proofs, and a proof can be redeemed again while its challenge is still valid. A seller with
149
+ more than one process, or one that restarts, needs a durable, atomic uniqueness check shared by
150
+ every worker (for example a unique key on network plus transaction signature, as an
151
+ authoritative settlement table provides). Rejecting expired challenges narrows the window but
152
+ does not close it on its own.
153
+
154
+ ## Credits for signed calls with no completion
155
+
156
+ ```ts
157
+ import { MppPaidNoCompletionError, readCredits } from "@twzrd1111/clawrouter-mpp";
158
+
159
+ try {
160
+ await payFetch(url, init);
161
+ } catch (error) {
162
+ if (error instanceof MppPaidNoCompletionError) {
163
+ console.log(readCredits()); // immutable snapshots; do not log credentials
164
+ }
165
+ throw error;
166
+ }
167
+ ```
168
+
169
+ Both rails record `{ payer, amount, challengeId, reason }` on a signed call's HTTP error or
170
+ transport failure. Payer comes from the supplied account/signer, never the seller's recipient.
171
+ Amount is the original integer base-unit string. Repeating a payer/challenge records one row.
172
+
173
+ When the proxy detects a provider error hidden inside HTTP 200, it calls the rail's
174
+ `onNoCompletion(response, reason)` hook. For a signed response this records the credit and
175
+ throws the same terminal error before any fallback model is paid. Unsigned responses retain
176
+ the ordinary provider fallback behavior. A custom host using these rails can invoke that
177
+ hook after its own completion validation; the response must be the one returned by the rail.
178
+
179
+ For a broken or unfinished native body the proxy invokes that same hook, records one credit,
180
+ and ends the existing SSE response with `payment_settled_no_completion`. Its HTTP 200 and
181
+ receipt have already been sent, so this is an in-stream error rather than a new HTTP 502.
182
+ The proxy never retries a model or pays a fallback after committing that stream. Disconnects
183
+ and request deadlines cancel the upstream reader and clean up coalesced callers.
184
+
185
+ `CreditLedger` and `creditPaidNoCompletion(ledger, error, fields)` are also public for callers
186
+ with their own ledger; `readCredits(ledger)` reads that ledger. Retrying the credit operation
187
+ returns the same immutable row and invokes no signer. It does not retry a model request.
188
+ Each row carries the payment `reference` from the error (or from `fields.reference`) when one
189
+ is known, so a credit can be joined to the error and the spend record. The reference is a hash
190
+ of the signed payload, not a chain signature; an unknown reference is left off the row.
37
191
 
38
- "Signed" is not "settled": on Solana the seller submits the transaction. Not covered: failures that happen
39
- mid-stream, and seller-side refunds.
192
+ Both ledgers are in memory, scoped to one process, and reset on restart. Credits record
193
+ signed attempts, not confirmed settlement or redeemable balances. There is no refund, chain
194
+ write, cross-process exclusion, persistence, or automatic reconciliation in these helpers.
40
195
 
41
196
  ## Develop
42
197
 
43
198
  ```
44
199
  npm install # from the ClawRouter repo root
200
+ # Before the candidate core is published, install rails development dependencies:
201
+ npm install --legacy-peer-deps # in packages/clawrouter-mpp
45
202
  npm run typecheck && npm test && npm run build # in packages/clawrouter-mpp
46
203
  ```
47
204
 
package/dist/index.d.ts CHANGED
@@ -1,14 +1,119 @@
1
1
  import { Account } from 'viem';
2
- import { SpendControl, PayFetch } from '@twzrd1111/clawrouter';
2
+ import { PayFetch, SpendControl, SpendRecord } from '@twzrd1111/clawrouter';
3
3
  export { PayFetch } from '@twzrd1111/clawrouter';
4
- import { SolanaNetwork } from '@solana/pay-kit/client';
5
4
  import { KeyPairSigner } from '@solana/kit';
6
5
 
6
+ declare const MPP_FULFILLMENT_RECORD_SCHEMA = "twzrd.mpp.fulfillment.v2";
7
+ declare const MPP_FULFILLMENT_MAX_BODY_BYTES: number;
8
+ type MppPaymentFacts = {
9
+ challengeId: string;
10
+ method: string;
11
+ intent: string;
12
+ payTo: string | null;
13
+ network: string | null;
14
+ asset: string | null;
15
+ amountAtomic: string | null;
16
+ };
17
+ type MppFulfillmentRecord = {
18
+ schema: typeof MPP_FULFILLMENT_RECORD_SCHEMA;
19
+ at_unix_ms: number;
20
+ resource_origin: string | null;
21
+ http_status: number;
22
+ payment_method: string;
23
+ payment_intent: string;
24
+ challenge_id: string;
25
+ paid_to: string | null;
26
+ network: string | null;
27
+ asset: string | null;
28
+ amount_atomic: string | null;
29
+ receipt_state: "valid_format" | "missing" | "invalid_format" | "unavailable";
30
+ receipt_status: string | null;
31
+ receipt_method: string | null;
32
+ /** Seller-supplied MPP receipt reference (EVM hash or Solana signature); not chain-verified here. */
33
+ receipt_reference: string | null;
34
+ receipt_timestamp: string | null;
35
+ requested_model: string | null;
36
+ /** Model label reported by the seller. This is not an attestation of model identity. */
37
+ seller_reported_model: string | null;
38
+ /** Equality of seller-reported and requested labels only; this does not verify the served model. */
39
+ seller_reported_model_matches_request: boolean | null;
40
+ gate: MppGateDecision | null;
41
+ /**
42
+ * Byte length of the complete response body as received. null when the recorder did not read
43
+ * the body, it exceeded MPP_FULFILLMENT_MAX_BODY_BYTES, or it was cancelled or errored first.
44
+ */
45
+ response_bytes: number | null;
46
+ /** Lowercase hex SHA-256 of the same complete body; null under the same conditions. */
47
+ body_sha256: string | null;
48
+ };
49
+ /** Size and digest of a complete response body, or null fields when it was not fully read. */
50
+ type MppBodyDigest = {
51
+ response_bytes: number | null;
52
+ body_sha256: string | null;
53
+ };
54
+ type MppFulfillmentSink = (record: MppFulfillmentRecord) => void | Promise<void>;
55
+
56
+ /** Optional buyer-side policy hook evaluated on the exact MPP challenge before signing. */
57
+ type MppGateChallenge = {
58
+ id: string;
59
+ realm?: string;
60
+ method: string;
61
+ intent: string;
62
+ request: Record<string, unknown>;
63
+ expires?: string;
64
+ description?: string;
65
+ };
66
+ /** Safe decision metadata retained with the paid response for receipt verification. */
67
+ type MppGateDecision = {
68
+ disposition: "allow" | "warn" | "block";
69
+ decisionId?: string;
70
+ intentHash?: string;
71
+ reasonCodes?: readonly string[];
72
+ };
73
+ /** Return the TWZRD gate's result. Throwing or returning an invalid result fails closed. */
74
+ type MppGateEvaluator = (challenge: MppGateChallenge) => MppGateDecision | Promise<MppGateDecision>;
75
+ /** Integration contract accepted by the MPP factories. The marker is not an authenticity proof. */
76
+ type MppIntegration = Readonly<{
77
+ twzrdMppIntegration: true;
78
+ gate: MppGateEvaluator;
79
+ onFulfillment: MppFulfillmentSink;
80
+ flush: () => Promise<void>;
81
+ }>;
82
+ declare class MppGateError extends Error {
83
+ readonly code: "MPP_GATE_BLOCKED" | "MPP_GATE_INVALID_RESULT";
84
+ readonly challengeId: string;
85
+ readonly decisionId?: string;
86
+ readonly reasonCodes?: readonly string[];
87
+ constructor(code: MppGateError["code"], challengeId: string, message: string, decision?: MppGateDecision);
88
+ }
89
+
90
+ type MppFlushOptions = {
91
+ /**
92
+ * Bound the entire wait for fulfillment records and ledger persistence. At the deadline,
93
+ * ledger flushing is still attempted in the background; neither writes nor response bodies
94
+ * are cancelled. Late ledger failures are retained for the next flush(). Omit to wait forever.
95
+ * Valid range: 0 through 2_147_483_647 milliseconds (Node's maximum timer delay).
96
+ */
97
+ timeoutMs?: number;
98
+ };
99
+ /** A rail fetch whose flush() accepts an optional deadline. Assignable to PayFetch. */
100
+ type MppPayFetch = PayFetch & {
101
+ flush: (options?: MppFlushOptions) => Promise<void>;
102
+ };
103
+ /** The deadline elapsed; persistence may still be running and response bodies remain caller-owned. */
104
+ declare class MppFlushTimeoutError extends Error {
105
+ readonly pending: number;
106
+ readonly timeoutMs: number;
107
+ readonly ledgerPending: boolean;
108
+ constructor(pending: number, timeoutMs: number, ledgerPending?: boolean);
109
+ }
110
+
7
111
  /**
8
112
  * Tempo TIP-20 USD stablecoins priced here, lower-case, all 6 decimals (the challenge carries no
9
113
  * `decimals`). Any other asset cannot be priced, so it is refused while amount limits are set.
10
114
  * - 0x20c0…0000 pathUSD (Moderato testnet 42431, also on mainnet 4217)
11
- * - 0x20c0…b9537d11c60e8b50 the USD token a Tempo-run MPP proxy quoted on mainnet 4217 (2026-11-03)
115
+ * - 0x20c0…b9537d11c60e8b50 fee token a Tempo-run MPP proxy quoted on mainnet 4217 (2026-11-03)
116
+ * - 0x20c0…f37de3740adec032 charge currency on that same OpenRouter challenge (6 decimals, 750 = 0.00075 USD)
12
117
  */
13
118
  declare const DEFAULT_TEMPO_USD_ASSETS: ReadonlySet<string>;
14
119
  type MppFetchOptions = {
@@ -16,10 +121,17 @@ type MppFetchOptions = {
16
121
  spend?: SpendControl;
17
122
  /** Override the assets treated as 6-decimal USD (lower-case addresses). */
18
123
  usdAssets?: ReadonlySet<string>;
124
+ /** Required branded object from createTwzrdMppIntegration. */
125
+ integration: MppIntegration;
19
126
  };
20
127
  type ChallengeLike = {
21
128
  id: string;
22
- request?: Record<string, unknown>;
129
+ realm?: string;
130
+ method: string;
131
+ intent: string;
132
+ request: Record<string, unknown>;
133
+ expires?: string;
134
+ description?: string;
23
135
  };
24
136
  /** The quote as the spend-control layer understands it. `amount` stays undefined if unpriceable. */
25
137
  declare function quoteFromChallenge(challenge: ChallengeLike, usdAssets?: ReadonlySet<string>): {
@@ -29,36 +141,41 @@ declare function quoteFromChallenge(challenge: ChallengeLike, usdAssets?: Readon
29
141
  amount: string | undefined;
30
142
  priceable: boolean;
31
143
  };
32
- type MethodLike = {
33
- createCredential: (options: any) => Promise<string>;
34
- };
35
- /**
36
- * Put the spend policy in the signing path of an mppx method.
37
- *
38
- * mppx's `challenge.received` event is an observer: it runs each handler inside try/catch and goes on to
39
- * sign, so a handler cannot veto a payment by throwing (verified against mppx 0.17.0 and live, 2026-11-03).
40
- * `createCredential` is the one place a throw really aborts: reserve (fail-closed) -> sign -> settle,
41
- * and release the reservation if signing fails. A signed credential counts as spend even if the request
42
- * then fails, because MPP settles before the upstream responds.
43
- */
44
- declare function guardMethod<M extends MethodLike>(method: M, control: SpendControl | undefined, usdAssets?: ReadonlySet<string>): M;
45
- declare function createMppFetch(account: Account, base?: typeof fetch, opts?: MppFetchOptions): PayFetch;
144
+ declare function createMppFetch(account: Account, base: typeof fetch | undefined, opts: MppFetchOptions): MppPayFetch;
46
145
 
47
- /**
48
- * 6-decimal USD stablecoin mints priced here (from pay-kit 0.13.1's own tables). Any other mint cannot be
49
- * priced, so it is refused while amount limits are set. Pass `usdMints` to add e.g. a localnet test mint.
50
- */
51
146
  declare const DEFAULT_SOLANA_USD_MINTS: ReadonlySet<string>;
147
+ type SolanaNetwork = "mainnet" | "devnet" | "localnet";
52
148
  type SolanaMppFetchOptions = {
53
149
  signer: KeyPairSigner;
54
150
  rpcUrl: string;
55
151
  network: SolanaNetwork;
56
- /** When set, every payment is checked against these limits before it is signed. */
57
152
  spend?: SpendControl;
58
- /** Extra mints to treat as 6-decimal USD (e.g. a localnet test mint). */
59
153
  usdMints?: Iterable<string>;
154
+ /** Required branded object from createTwzrdMppIntegration. */
155
+ integration: MppIntegration;
156
+ /** Injectable transport; defaults to fetch. The SDK's RPC transport is separate. */
157
+ fetch?: typeof fetch;
60
158
  };
61
- declare function createSolanaMppFetch(opts: SolanaMppFetchOptions): Promise<PayFetch>;
159
+ declare function createSolanaMppFetch(opts: SolanaMppFetchOptions): Promise<MppPayFetch>;
160
+
161
+ /** In-memory credit row. No chain write and no signer. */
162
+ type CreditEntry = Readonly<{
163
+ payer: string;
164
+ amount: string;
165
+ challengeId: string;
166
+ reason: string;
167
+ /**
168
+ * Payment reference from MppPaidNoCompletionError, when one is known: `mpp-` plus a short hash of
169
+ * the signed payload. Joins the row to the error and the spend record. Not a chain tx signature.
170
+ */
171
+ reference?: string;
172
+ }>;
173
+ declare class CreditLedger {
174
+ private readonly rows;
175
+ /** One row per payer and challenge. A retry returns the first row. */
176
+ recordOnce(entry: CreditEntry): CreditEntry;
177
+ read(): readonly CreditEntry[];
178
+ }
62
179
 
63
180
  declare class MppPaidNoCompletionError extends Error {
64
181
  readonly reference: string;
@@ -67,6 +184,66 @@ declare class MppPaidNoCompletionError extends Error {
67
184
  cause?: unknown;
68
185
  });
69
186
  }
187
+ /**
188
+ * Record one credit for a paid call that returned no completion.
189
+ * A retry of the same payer and challenge returns the first row.
190
+ * The row carries `fields.reference`, else the error's reference when one is known.
191
+ * `sign`, when passed, is never called.
192
+ */
193
+ declare function creditPaidNoCompletion(ledger: CreditLedger, err: MppPaidNoCompletionError, fields: CreditEntry, hooks?: {
194
+ sign?: () => Promise<unknown>;
195
+ }): CreditEntry;
196
+ type PaidCall = {
197
+ /** The paying account supplied to the rail, never the challenge recipient. */
198
+ payer: string;
199
+ /** A credential or transaction was produced for this call. */
200
+ signed: boolean;
201
+ /** Stable, non-secret handle for the payment (hash of what was sent), for disputes. */
202
+ reference?: string;
203
+ /** The ledger entry written when the payment was signed, if limits made one. */
204
+ record?: SpendRecord;
205
+ /** Payer, amount, and challenge known before the model runs. Reason is filled on failure. */
206
+ credit?: Pick<CreditEntry, "payer" | "amount" | "challengeId">;
207
+ /** Optional pre-sign policy decision; no raw challenge or signed token is retained. */
208
+ gate?: MppGateDecision;
209
+ payment?: MppPaymentFacts;
210
+ };
211
+ /** Rows written by `fail` when a signed call returns no completion. */
212
+ declare function readCredits(ledger?: CreditLedger): readonly CreditEntry[];
213
+ /** Safe payment facts associated with this exact response, if it was paid by this rail. */
214
+ declare function readPaidCallEvidence(response: Response): Pick<PaidCall, "payer" | "reference" | "credit" | "gate" | "payment"> | undefined;
215
+
216
+ /**
217
+ * Seller-side spent-proof ledger. In memory only.
218
+ *
219
+ * A proof is one payment transaction signature plus one challenge id.
220
+ * The buyer's 402 followed by the authorized retry is one proof: the 402
221
+ * carries no transaction signature, so it never claims a slot.
222
+ */
223
+ declare class ReplayRejectedError extends Error {
224
+ readonly networkId: string;
225
+ readonly txSignature: string;
226
+ readonly challengeId: string;
227
+ readonly status = 409;
228
+ readonly code = "proof_replayed";
229
+ constructor(networkId: string, txSignature: string, challengeId: string);
230
+ }
231
+ declare class SpentProofLedger {
232
+ private readonly transactions;
233
+ private readonly challenges;
234
+ claim(networkId: string, txSignature: string, challengeId: string): boolean;
235
+ }
236
+ /**
237
+ * Accept one paid proof and call the model at most once.
238
+ * The claim is synchronous, so two overlapping calls yield one winner.
239
+ */
240
+ declare function fulfillPaidProof<T>(opts: {
241
+ ledger: SpentProofLedger;
242
+ networkId: string;
243
+ txSignature: string;
244
+ challengeId: string;
245
+ upstream: () => Promise<T>;
246
+ }): Promise<T>;
70
247
 
71
- export { DEFAULT_SOLANA_USD_MINTS, DEFAULT_TEMPO_USD_ASSETS, MppPaidNoCompletionError, createMppFetch, createSolanaMppFetch, guardMethod, quoteFromChallenge };
72
- export type { MppFetchOptions, SolanaMppFetchOptions };
248
+ export { CreditLedger, DEFAULT_SOLANA_USD_MINTS, DEFAULT_TEMPO_USD_ASSETS, MPP_FULFILLMENT_MAX_BODY_BYTES, MPP_FULFILLMENT_RECORD_SCHEMA, MppFlushTimeoutError, MppGateError, MppPaidNoCompletionError, ReplayRejectedError, SpentProofLedger, createMppFetch, createSolanaMppFetch, creditPaidNoCompletion, fulfillPaidProof, quoteFromChallenge, readCredits, readPaidCallEvidence };
249
+ export type { CreditEntry, MppBodyDigest, MppFetchOptions, MppFlushOptions, MppFulfillmentRecord, MppFulfillmentSink, MppGateChallenge, MppGateDecision, MppGateEvaluator, MppIntegration, MppPayFetch, MppPaymentFacts, SolanaMppFetchOptions };