Skip to content

Potential settlement-ordering issue: agent stream is delivered before payment settlement #6

Description

@chenshj73

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions