Potential settlement-ordering issue: agent stream is delivered before payment settlement
Hi, I noticed a possible payment-flow issue in the current repository state. This is a conservative report based on the visible code path, and I may be missing deployment-specific guards outside this repository.
Repository: https://github.com/tangle-network/agent-gateway
Reviewed HEAD: 88bddab
What I observed
The gateway authenticates X-Payment-Signature or an MPP credential, then streams the sandbox/agent response to the caller. The settlement helper is documented and implemented as something called after the response stream has drained, and settlement failure is caught and reported to the observer rather than preventing the already-delivered output.
Relevant code excerpts:
src/dispatch.ts:81-112
81 // Payment / auth.
82 const spendAuthHeader = c.req.header('X-Payment-Signature')
83 const authHeader = c.req.header('Authorization') ?? ''
...
88 if (spendAuthHeader) {
89 const signer = await verifyX402(spendAuthHeader, config.x402, state.nonceStore)
...
110 consumerId = signer
111 paymentMethod = 'x402'
112 } else if (config.mpp && authHeader.toLowerCase().startsWith('payment ')) {
src/dispatch.ts:369-402
369 export async function* dispatchSandboxStreamRich(
...
377 const box = await config.getSandbox(agent)
378 const promptStream = box.streamPrompt(userMessage, {
379 sessionId: sessionId ?? `consumer:${consumerId}`,
380 systemPrompt: agent.systemPrompt,
381 })
382 for await (const event of promptStream) {
...
389 yield {
390 kind: 'text',
391 delta: redactSystemPromptFromOutput(event.data.delta, agent.systemPrompt),
392 }
src/dispatch.ts:404-459
404 /**
405 * Record usage event + settle payment + invoke the observer. Both wire
406 * formats call this once their stream has drained, so settlement happens
407 * exactly once per request regardless of protocol.
408 */
409 export async function settleAndRecord(
...
440 if (config.settlePayment) {
441 await config
442 .settlePayment(
...
448 totalCost,
449 )
450 .catch(async (err) => {
451 const msg = err instanceof Error ? err.message : String(err)
452 console.error(`[agent-gateway] settlement failed for ${authz.consumerId}: ${msg}`)
453 await obs?.onSettlementError?.(ctx, {
Why this may matter
The paid sink is the streamed agent output. If settlement happens only after the stream is consumed, then a settlement failure cannot prevent the caller from receiving the paid result. The code also catches settlement errors locally, which makes the failure observable but not atomic with content delivery.
Suggested check
Consider settling or reserving funds before streaming paid output, or buffering/withholding the final paid result until settlement succeeds. If post-stream settlement is intentional, documenting it as a credit-risk model and adding retry/debt handling could make the behavior explicit.
Conservative caveat
I did not exercise a live deployment, so this report is based on the current source ordering: verify/authorize -> stream paid output -> settle.
Potential settlement-ordering issue: agent stream is delivered before payment settlement
Hi, I noticed a possible payment-flow issue in the current repository state. This is a conservative report based on the visible code path, and I may be missing deployment-specific guards outside this repository.
Repository:
https://github.com/tangle-network/agent-gatewayReviewed HEAD:
88bddabWhat I observed
The gateway authenticates
X-Payment-Signatureor an MPP credential, then streams the sandbox/agent response to the caller. The settlement helper is documented and implemented as something called after the response stream has drained, and settlement failure is caught and reported to the observer rather than preventing the already-delivered output.Relevant code excerpts:
src/dispatch.ts:81-112src/dispatch.ts:369-402src/dispatch.ts:404-459Why this may matter
The paid sink is the streamed agent output. If settlement happens only after the stream is consumed, then a settlement failure cannot prevent the caller from receiving the paid result. The code also catches settlement errors locally, which makes the failure observable but not atomic with content delivery.
Suggested check
Consider settling or reserving funds before streaming paid output, or buffering/withholding the final paid result until settlement succeeds. If post-stream settlement is intentional, documenting it as a credit-risk model and adding retry/debt handling could make the behavior explicit.
Conservative caveat
I did not exercise a live deployment, so this report is based on the current source ordering: verify/authorize -> stream paid output -> settle.